Compare commits

...

16 Commits

Author SHA1 Message Date
syuilo
db8b824b84 12.60.0 2020-11-25 21:41:50 +09:00
syuilo
b71f62535d New Crowdin updates (#6851)
* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)
2020-11-25 21:36:55 +09:00
syuilo
e9a1e281b9 Fix #6854 2020-11-25 21:33:57 +09:00
syuilo
0144408500 nanka iroiro (#6853)
* wip

* Update maps.ts

* wip

* wip

* wip

* wip

* Update base.vue

* wip

* wip

* wip

* wip

* Update link.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update privacy.vue

* wip

* wip

* wip

* wip

* Update range.vue

* wip

* wip

* wip

* wip

* Update profile.vue

* wip

* Update a.vue

* Update index.vue

* wip

* Update sidebar.vue

* wip

* wip

* Update account-info.vue

* Update a.vue

* wip

* wip

* Update sounds.vue

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Update account-info.vue

* Update account-info.vue

* wip

* wip

* wip

* Update d-persimmon.json5

* wip
2020-11-25 21:31:34 +09:00
syuilo
7660839e40 フォントレンダリングを調整 2020-11-22 17:58:08 +09:00
syuilo
d1ca851ebe Update webpack 2020-11-21 00:48:00 +09:00
syuilo
35a281d443 Update reversi maps 2020-11-19 17:16:48 +09:00
syuilo
c2690fff47 ✌️ 2020-11-19 14:11:29 +09:00
syuilo
c3a73a41d1 12.59.0 2020-11-18 13:06:32 +09:00
syuilo
b72baa3295 New Crowdin updates (#6841)
* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Arabic)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Ukrainian)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Chinese Traditional)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (French)
2020-11-18 12:40:40 +09:00
syuilo
73ce22c8a4 Update dependencies 🚀 2020-11-18 12:19:11 +09:00
syuilo
c4f7e6659f Improve reaction picker 2020-11-18 12:09:14 +09:00
syuilo
0739ae006d Improve usability 2020-11-18 11:21:35 +09:00
syuilo
eaa92e784d Resolve #6840 2020-11-17 22:52:07 +09:00
syuilo
48589e0da1 12.58.0 2020-11-17 15:04:15 +09:00
syuilo
0044d83801 nanka iroiro (#6847)
* wip

* wip

* wip

* wip

* Update ja-JP.yml

* wip

* wip

* wip
2020-11-17 14:59:15 +09:00
140 changed files with 5725 additions and 1974 deletions

View File

@@ -380,6 +380,7 @@ smtpHost: "المضيف"
smtpUser: "اسم المستخدم"
smtpPass: "الكلمة السرية"
display: "المظهر"
public: "للعامة"
_mfm:
mention: "أشر الى"
quote: "اقتبس"

View File

@@ -95,6 +95,7 @@ sensitive: "NSFW"
add: "Hinzufügen"
reaction: "Reaktionen"
reactionSettingDescription: "Gib deine Lieblingsreaktionen ein, um sie der Reaktionsauswahl hinzuzufügen."
reactionSettingDescription2: "Ziehen zum Reorganisieren, Klicken zum Löschen."
rememberNoteVisibility: "Notizsichtbarkeit merken"
attachCancel: "Anhang entfernen"
markAsSensitive: "Als NSFW markieren"
@@ -124,6 +125,7 @@ settingGuide: "Empfohlene Einstellung"
cacheRemoteFiles: "Dateien von anderen Instanzen im Cache speichern"
cacheRemoteFilesDescription: "Wenn diese Einstellung deaktiviert ist, werden Dateien anderer Instanzen direkt von dort geladen. Hierdurch wird Speicherplatz gespart, aber mehr Bandbreite verbraucht, da keine Vorschaubilder generiert werden."
flagAsBot: "Als Bot markieren"
flagAsBotDescription: "Wenn dieser Account durch ein Programm gesteuert wird, setze diesen Haken. Falls aktiviert, agiert es als Flag für andere Entwickler um endlose Kettenreaktionen mit anderen Bots zu verhindern und lässt Misskey's interne Systeme diesen Account als Bot behandeln."
flagAsCat: "Als Katze markieren"
autoAcceptFollowed: "Follow-Anfragen automatisch akzeptieren"
addAcount: "Benutzerkonto hinzufügen"
@@ -315,6 +317,8 @@ bannerUrl: "Banner-URL"
basicInfo: "Basisdaten"
pinnedUsers: "Angepinnte Benutzer"
pinnedUsersDescription: "Gib einen Benutzernamen pro Zeile ein. Diese werden im \"Erkunden\" Tab angezeigt."
pinnedPages: "Angepinnte Seiten"
pinnedPagesDescription: "Gib hier die Pfäde zu den Seiten an, die du an die Spitze dieser Instanz anheften möchtest, getrennt durch neue Zeilen."
hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptcha aktivieren"
hcaptchaSiteKey: "Site key"
@@ -543,6 +547,11 @@ deck: "Deck"
undeck: "Deck verlassen"
useBlurEffectForModal: "Weichzeichnungseffekt für Modals verwenden"
useFullReactionPicker: "Vollständige Reaktionsauswahl nutzen"
width: "Breite"
height: "Höhe"
large: "Groß"
medium: "Mittel"
small: "Klein"
generateAccessToken: "Zugriffstoken generieren"
permission: "Berechtigungen"
enableAll: "Alle aktivieren"
@@ -605,6 +614,11 @@ random: "Zufällig"
system: "System"
switchUi: "UI wechseln"
desktop: "Desktop"
clip: "Clip"
createNew: "Neu erstellen"
optional: "Optional"
createNewClip: "Neuen Clip erstellen"
public: "Öffentlich"
_mfm:
cheatSheet: "MFM Spickzettel"
intro: "MFM ist eine an vielen Stellen verwendbare und Misskey-exklusive Markup-Sprache. Hier kannst du eine Liste von verfügbarer MFM-Syntax anschauen."
@@ -1070,6 +1084,7 @@ _pages:
created: "Seite erfolgreich erstellt"
updated: "Seite erfolgreich aktualisiert"
deleted: "Seite erfolgreich gelöscht"
pageSetting: "Seiteneinstellungen"
nameAlreadyExists: "Die angegebene Seiten-URL existiert bereits"
invalidNameTitle: "Die angegebene Seiten-URL ist ungültig"
invalidNameText: "Überprüfe, ob der Seitentitel nicht leer ist"
@@ -1080,6 +1095,7 @@ _pages:
unlike: "\"Gefällt mir\" entfernen"
my: "Meine Seiten"
liked: "Seiten, die mir gefallen"
featured: "Beliebt"
inspector: "Inspektor"
contents: "Inhalt"
content: "Inhalt"
@@ -1105,7 +1121,7 @@ _pages:
text: "Text"
textarea: "Textfeld"
section: "Abschnitt"
image: "Bilder"
image: "Bild"
button: "Knopf"
if: "Falls"
_if:
@@ -1120,7 +1136,7 @@ _pages:
name: "Variablenname"
text: "Titel"
default: "Standardwert"
textareaInput: "Eingabe des mehrzeiligen Textfelds"
textareaInput: "Mehrzeiliges Texteingabefeld"
_textareaInput:
name: "Variablenname"
text: "Titel"
@@ -1135,6 +1151,11 @@ _pages:
id: "Leinwand-ID"
width: "Breite"
height: "Höhe"
note: "Eingebettete Notiz"
_note:
id: "Notiz ID"
idDescription: "Du kannst alternativ auch die Notiz-URL angeben."
detailed: "Detailierte Ansicht"
switch: "Fallunterscheidung"
_switch:
name: "Variablenname"

View File

@@ -95,6 +95,7 @@ sensitive: "NSFW"
add: "Add"
reaction: "Reaction"
reactionSettingDescription: "Assign your favorite reactions which want to pin in reaction picker."
reactionSettingDescription2: "Drag to reorganize, click to delete."
rememberNoteVisibility: "Remember note visibility settings"
attachCancel: "Remove attachment"
markAsSensitive: "Mark as NSFW"
@@ -124,6 +125,7 @@ settingGuide: "Suggested Configuration"
cacheRemoteFiles: "Cache remote files"
cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but will increase traffic, because thumbnails will not be generated."
flagAsBot: "This account is a bot"
flagAsBotDescription: "If this account is controlled by a program, set this option. If enabled, it will act as flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot."
flagAsCat: "This account is a cat"
autoAcceptFollowed: "Automatically approve follow requests from users you're following"
addAcount: "Add Account"
@@ -315,6 +317,8 @@ bannerUrl: "Banner image URL"
basicInfo: "Basic info"
pinnedUsers: "Pinned user"
pinnedUsersDescription: "List one username per line. Users listed here will be pinned under \"Explore\" tab."
pinnedPages: "Pinned pages"
pinnedPagesDescription: "Enter the paths of the pages you want to pin to the top page of this instance, separated by new lines."
hcaptcha: "hCaptcha"
enableHcaptcha: "Enable hCaptcha"
hcaptchaSiteKey: "Site key"
@@ -543,6 +547,11 @@ deck: "Deck"
undeck: "Leave Deck"
useBlurEffectForModal: "Use blur effect for modals"
useFullReactionPicker: "Use full-size reaction picker"
width: "Width"
height: "Height"
large: "Big"
medium: "Medium"
small: "Small"
generateAccessToken: "Generate access token"
permission: "Permissions"
enableAll: "Enable all"
@@ -605,6 +614,11 @@ random: "Random"
system: "System"
switchUi: "Switch UI"
desktop: "Desktop"
clip: "Clip"
createNew: "Create new"
optional: "Optional"
createNewClip: "Create new clip"
public: "Public"
_mfm:
cheatSheet: "MFM Cheatsheet"
intro: "MFM is a Misskey-exclusive markup language that can be used in many places. Here you can view a list of all available MFM syntax."
@@ -1070,6 +1084,7 @@ _pages:
created: "Successfully created a page!"
updated: "Successfully updated the page!"
deleted: "The page has been deleted"
pageSetting: "Page settings"
nameAlreadyExists: "The specified page URL already exists"
invalidNameTitle: "The specified page URL is invalid"
invalidNameText: "Check whether that is not a blank"
@@ -1080,6 +1095,7 @@ _pages:
unlike: "Undo like"
my: "My pages"
liked: "Liked pages"
featured: "Featured"
inspector: "Inspector"
contents: "Content"
content: "Page block"
@@ -1135,6 +1151,11 @@ _pages:
id: "Canvas ID"
width: "Width"
height: "Height"
note: "Embedded note"
_note:
id: "Note ID"
idDescription: "You can also paste the Note's URL to set it instead."
detailed: "Detailed view"
switch: "Switch"
_switch:
name: "Variable name"

View File

@@ -542,6 +542,8 @@ deck: "Deck"
undeck: "Quitar deck"
useBlurEffectForModal: "Usar efecto borroso en modales"
useFullReactionPicker: "Reacción"
width: "Ancho"
height: "Altura"
generateAccessToken: "Generar token de acceso"
permission: "Permisos"
enableAll: "Activar todo"
@@ -604,6 +606,7 @@ random: "Aleatorio"
system: "Sistema"
switchUi: "Cambiar interfaz de usuario"
desktop: "Escritorio"
public: "Público"
_mfm:
cheatSheet: "Hoja de referencia de MFM"
intro: "MFM es un lenguaje de marcado dedicado que se puede usar en varios lugares dentro de Misskey. Aquí puede ver una lista de sintaxis disponibles en MFM."

View File

@@ -538,6 +538,8 @@ pluginInstallWarn: "Ninstallez que des extensions provenant de sources de con
deck: "Deck"
undeck: "Quitter le deck"
useBlurEffectForModal: "Utiliser un effet de flou pour les modals"
width: "Largeur"
height: "Hauteur"
generateAccessToken: "Générer un jeton d'accès"
permission: "Autorisations "
enableAll: "Tout activer"
@@ -581,6 +583,7 @@ setMultipleBySeparatingWithSpace: "Vous pouvez définir plus dun, séparés p
fileIdOrUrl: "ID du fichier ou URL"
chatOpenBehavior: "Comportement de la fenêtre de discussion lors de son ouverture"
random: "Aléatoire"
public: "Public"
_mfm:
mention: "Mentionner"
hashtag: "Hashtags"

View File

@@ -125,7 +125,9 @@ settingGuide: "おすすめ設定"
cacheRemoteFiles: "リモートのファイルをキャッシュする"
cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。"
flagAsBot: "Botとして設定"
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
flagAsCat: "Catとして設定"
flagAsCatDescription: "このアカウントが猫であることを示す場合は、このフラグをオンにします。"
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
addAcount: "アカウント追加"
loginFailed: "ログインに失敗しました"
@@ -316,6 +318,8 @@ bannerUrl: "バナー画像のURL"
basicInfo: "基本情報"
pinnedUsers: "ピン留めユーザー"
pinnedUsersDescription: "「みつける」ページなどにピン留めしたいユーザーを改行で区切って記述します。"
pinnedPages: "ピン留めページ"
pinnedPagesDescription: "インスタンスのトップページにピン留めしたいページのパスを改行で区切って記述します。"
hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptchaを有効にする"
hcaptchaSiteKey: "サイトキー"
@@ -437,6 +441,7 @@ useOsNativeEmojis: "OSネイティブの絵文字を使用"
youHaveNoGroups: "グループがありません"
joinOrCreateGroup: "既存のグループに招待してもらうか、新しくグループを作成してください。"
noHistory: "履歴はありません"
signinHistory: "ログイン履歴"
disableAnimatedMfm: "動きのあるMFMを無効にする"
doing: "やっています"
category: "カテゴリ"
@@ -489,6 +494,7 @@ none: "なし"
showInPage: "ページで表示"
popout: "ポップアウト"
volume: "音量"
masterVolume: "マスター音量"
details: "詳細"
chooseEmoji: "絵文字を選択"
unableToProcess: "操作を完了できません"
@@ -544,6 +550,11 @@ deck: "デッキ"
undeck: "デッキ解除"
useBlurEffectForModal: "モーダルにぼかし効果を使用"
useFullReactionPicker: "フル機能リアクションピッカーを使用"
width: "幅"
height: "高さ"
large: "大"
medium: "中"
small: "小"
generateAccessToken: "アクセストークンの発行"
permission: "権限"
enableAll: "全て有効にする"
@@ -556,7 +567,8 @@ useStarForReactionFallback: "リアクション絵文字が不明な場合、代
emailConfig: "メールサーバー設定"
enableEmail: "メール配信機能を有効化する"
emailConfigInfo: "メールアドレスの確認やパスワードリセットの際に使います"
email: "メールアドレス"
email: "メール"
emailAddress: "メールアドレス"
smtpConfig: "SMTP サーバーの設定"
smtpHost: "ホスト"
smtpPort: "ポート"
@@ -588,6 +600,7 @@ regenerateLoginTokenDescription: "ログインに使用される内部トーク
setMultipleBySeparatingWithSpace: "スペースで区切って複数設定できます。"
fileIdOrUrl: "ファイルIDまたはURL"
chatOpenBehavior: "チャットを開くときの動作"
behavior: "動作"
sample: "サンプル"
abuseReports: "通報"
reportAbuse: "通報"
@@ -611,6 +624,42 @@ createNew: "新規作成"
optional: "任意"
createNewClip: "新しいクリップを作成"
public: "パブリック"
i18nInfo: "Misskeyは有志によって様々な言語に翻訳されています。{link}で翻訳に協力できます。"
manageAccessTokens: "アクセストークンの管理"
accountInfo: "アカウント情報"
notesCount: "ノートの数"
repliesCount: "返信した数"
renotesCount: "Renoteした数"
repliedCount: "返信された数"
renotedCount: "Renoteされた数"
followingCount: "フォロー数"
followersCount: "フォロワー数"
sentReactionsCount: "リアクションした数"
receivedReactionsCount: "リアクションされた数"
pollVotesCount: "アンケートに投票した数"
pollVotedCount: "アンケートに投票された数"
yes: "はい"
no: "いいえ"
driveFilesCount: "ドライブのファイル数"
driveUsage: "ドライブ使用量"
noCrawle: "クローラーによるインデックスを拒否"
noCrawleDescription: "検索エンジンにあなたのユーザーページ、ート、Pagesなどのコンテンツを登録(インデックス)しないよう要請します。"
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
alwaysMarkSensitive: "デフォルトでメディアを閲覧注意にする"
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
disableShowingAnimatedImages: "アニメーション画像を再生しない"
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"
notSet: "未設定"
emailVerified: "メールアドレスが確認されました"
noteFavoritesCount: "お気に入りノートの数"
pageLikesCount: "Pageにいいねした数"
pageLikedCount: "Pageにいいねされた数"
reversiCount: "リバーシの対局数"
_nsfw:
respect: "閲覧注意のメディアは隠す"
ignore: "閲覧注意のメディアを隠さない"
force: "常にメディアを隠す"
_mfm:
cheatSheet: "MFMチートシート"
@@ -737,6 +786,8 @@ _theme:
manage: "テーマの管理"
code: "テーマコード"
installed: "{name}をインストールしました"
installedThemes: "インストールされたテーマ"
builtinThemes: "標準のテーマ"
alreadyInstalled: "そのテーマは既にインストールされています"
invalid: "テーマの形式が間違っています"
make: "テーマを作る"
@@ -812,6 +863,8 @@ _sfx:
chatBg: "チャット(バックグラウンド)"
antenna: "アンテナ受信"
channel: "チャンネル通知"
reversiPutBlack: "リバーシ: 黒が打ったとき"
reversiPutWhite: "リバーシ: 白が打ったとき"
_ago:
unknown: "謎"
@@ -991,7 +1044,9 @@ _profile:
username: "ユーザー名"
description: "自己紹介"
youCanIncludeHashtags: "ハッシュタグを含めることができます。"
metadata: "補足情報"
metadata: "追加情報"
metadataEdit: "追加情報を編集"
metadataDescription: "プロフィールに表として4つまでの追加情報を表示することができます。"
metadataLabel: "ラベル"
metadataContent: "内容"
@@ -1117,6 +1172,7 @@ _pages:
unlike: "いいね解除"
my: "自分のページ"
liked: "いいねしたページ"
featured: "人気"
inspector: "インスペクター"
contents: "コンテンツ"
content: "ページブロック"

View File

@@ -16,6 +16,9 @@ noNotes: "노트가 없습니다"
noNotifications: "표시할 알림이 없습니다"
instance: "인스턴스"
settings: "설정"
basicSettings: "기본 설정"
otherSettings: "기타 설정"
openInWindow: "창으로 열기"
profile: "프로필"
timeline: "타임라인"
noAccountDescription: "자기소개가 없습니다"
@@ -40,6 +43,7 @@ deleteAndEditConfirm: "이 노트를 삭제한 뒤 다시 편집하시겠습니
addToList: "리스트에 추가"
sendMessage: "메시지 보내기"
copyUsername: "유저명 복사"
searchUser: "사용자 검색"
reply: "답글"
loadMore: "더 보기"
youGotNewFollower: "새로운 팔로워가 있습니다"
@@ -66,7 +70,11 @@ followers: "팔로워"
followsYou: "당신을 팔로우합니다"
createList: "리스트 만들기"
manageLists: "리스트 관리"
error: "오류"
somethingHappened: "오류가 발생했습니다"
retry: "다시 시도"
pageLoadError: "페이지를 불러오지 못했습니다."
pageLoadErrorDescription: "네트워크 연결 또는 브라우저 캐시로 인해 발생했을 가능성이 높습니다. 캐시를 삭제하거나, 잠시 후 다시 시도해 주세요."
enterListName: "리스트 이름을 입력"
privacy: "프라이버시"
makeFollowManuallyApprove: "팔로우를 수동으로 승인"
@@ -87,6 +95,7 @@ sensitive: "열람주의"
add: "추가"
reaction: "리액션"
reactionSettingDescription: "리액션 선택 상자에 표시할 리액션을 설정합니다."
reactionSettingDescription2: "드래그하여 순서를 바꿉니다. 클릭하면 삭제됩니다."
rememberNoteVisibility: "공개 범위를 기억하기"
attachCancel: "첨부 취소"
markAsSensitive: "열람주의로 설정"
@@ -105,6 +114,8 @@ unsuspendConfirm: "이 계정의 정지를 해제하시겠습니까?"
selectList: "리스트 선택"
selectAntenna: "안테나 선택"
selectWidget: "위젯 선택"
editWidgets: "위젯 편집"
editWidgetsExit: "편집 종료"
customEmojis: "커스텀 이모지"
emoji: "이모지"
emojiName: "이모지 이름"
@@ -114,6 +125,7 @@ settingGuide: "추천 설정"
cacheRemoteFiles: "리모트 파일을 캐시"
cacheRemoteFilesDescription: "이 설정을 해지하면 리모트 파일을 캐시하지 않고 해당 파일을 직접 링크하게 됩니다. 그에 따라 서버의 저장 공간을 절약할 수 있지만, 썸네일이 생성되지 않기 때문에 통신량이 증가합니다."
flagAsBot: "나는 봇입니다"
flagAsBotDescription: "이 계정을 자동화된 수단으로 운용할 경우에 활성화해 주세요. 이 플래그를 활성화하면, 다른 봇이 이를 참고하여 봇 끼리의 무한 연쇄 반응을 회피하거나, 이 계정의 시스템 상에서의 취급이 Bot 운영에 최적화되는 등의 변화가 생깁니다."
flagAsCat: "나는 고양이다냥"
autoAcceptFollowed: "팔로우 중인 유저로부터의 팔로우 요청을 자동 수락"
addAcount: "계정 추가"
@@ -205,6 +217,7 @@ remove: "삭제"
removed: "삭제하였습니다"
removeAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
deleteAreYouSure: "\"{x}\" 을(를) 삭제하시겠습니까?"
resetAreYouSure: "초기화 하시겠습니까?"
saved: "저장하였습니다"
messaging: "대화"
upload: "업로드"
@@ -263,6 +276,7 @@ rename: "이름 변경"
avatar: "아바타"
banner: "배너"
nsfw: "열람주의"
whenServerDisconnected: "서버와의 접속이 끊겼을 때"
disconnectedFromServer: "서버와의 연결이 끊어졌습니다"
reload: "새로고침"
doNothing: "무시하기"
@@ -303,6 +317,8 @@ bannerUrl: "배너 이미지 URL"
basicInfo: "기본 정보"
pinnedUsers: "고정된 유저"
pinnedUsersDescription: "\"발견하기\" 페이지 등에 고정하고 싶은 유저를 한 줄에 한 명씩 적습니다."
pinnedPages: "고정한 페이지"
pinnedPagesDescription: "인스턴스의 대문에 고정하고 싶은 페이지의 경로를 한 줄에 하나씩 적습니다."
hcaptcha: "hCaptcha"
enableHcaptcha: "hCaptcha 활성화"
hcaptchaSiteKey: "사이트 키"
@@ -441,6 +457,7 @@ remote: "리모트"
total: "합계"
weekOverWeekChanges: "지난주보다"
dayOverDayChanges: "어제보다"
clientSettings: "클라이언트 설정"
accountSettings: "계정 설정"
promotion: "프로모션"
promote: "프로모션하기"
@@ -463,6 +480,7 @@ objectStorageUseSSL: "SSL 사용"
objectStorageUseSSLDesc: "API 호출시 HTTPS 를 사용하지 않는 경우 OFF 로 설정해 주세요"
objectStorageUseProxy: "연결에 프록시를 사용"
objectStorageUseProxyDesc: "오브젝트 스토리지 API 호출시 프록시를 사용하지 않는 경우 OFF 로 설정해 주세요"
objectStorageSetPublicRead: "업로드할 때 'public-read'를 설정하기"
serverLogs: "서버 로그"
deleteAll: "모두 삭제"
showFixedPostForm: "타임라인 상단에 글 작성란을 표시"
@@ -470,6 +488,8 @@ newNoteRecived: "새 노트가 있습니다"
sounds: "소리"
listen: "듣기"
none: "없음"
showInPage: "페이지로 보기"
popout: "새 창으로 열기"
volume: "음량"
details: "자세히"
chooseEmoji: "이모지 선택"
@@ -524,11 +544,18 @@ plugins: "플러그인"
pluginInstallWarn: "신뢰할 수 없는 플러그인은 설치하지 마십시오."
deck: "덱"
undeck: "덱 해제"
width: "폭"
height: "높이"
large: "크게"
medium: "보통"
small: "작게"
generateAccessToken: "액세스 토큰 생성"
permission: "권한"
enableAll: "전체 선택"
disableAll: "전체 해제"
tokenRequested: "계정 접근 허용"
pluginTokenRequestedDescription: "이 플러그인은 여기서 설정한 권한을 사용할 수 있게 됩니다."
notificationType: "알림 유형"
edit: "편집"
useStarForReactionFallback: "알 수 없는 리액션 이모지 대신 ★ 사용"
emailConfig: "메일 서버 설정"
@@ -542,23 +569,93 @@ smtpPass: "비밀번호"
emptyToDisableSmtpAuth: "SMTP 인증을 사용하지 않으려면 공란으로 비워둡니다."
smtpSecure: "SMTP 연결에 Implicit SSL/TTS 사용"
smtpSecureInfo: "STARTTLS 사용 시에는 해제합니다."
testEmail: "이메일 전송 테스트"
wordMute: "단어 뮤트"
makeActive: "활성화"
display: "표시"
copy: "복사"
overview: "요약"
logs: "로그"
delayed: "지연"
database: "데이터베이스"
channel: "채널"
create: "생성"
notificationSetting: "알림 설정"
notificationSettingDesc: "표시할 알림의 종류를 선택해 주세요."
useGlobalSettingDesc: "활성화하면 계정의 알림 설정이 적용되니다. 비활성화하면 개별적으로 설정할 수 있게 됩니다."
other: "기타"
regenerateLoginToken: "로그인 토큰을 재생성"
regenerateLoginTokenDescription: "로그인할 때 사용되는 내부 토큰을 재생성합니다. 일반적으로 이 작업을 실행할 필요는 없습니다. 이 기능을 사용하면 이 계정으로 로그인한 모든 기기에서 로그아웃됩니다."
setMultipleBySeparatingWithSpace: "공백으로 구분하여 여러 개 설정할 수 있습니다."
fileIdOrUrl: "파일 ID 또는 URL"
chatOpenBehavior: "대화를 열 때의 동작"
sample: "예시"
abuseReports: "신고"
reportAbuse: "신고"
reportAbuseOf: "{name}을 신고하기"
fillAbuseReportDescription: "신고하려는 이유를 자세히 알려주세요. 특정 게시물을 신고할 때에는 게시물의 URL도 포함해 주세요."
abuseReported: "신고를 보냈습니다. 신고해 주셔서 감사합니다."
send: "전송"
random: "랜덤"
switchUi: "UI 전환"
desktop: "데스크탑"
clip: "클립"
createNew: "새로 만들기"
optional: "옵션"
createNewClip: "새 클립 만들기"
public: "공개"
_mfm:
cheatSheet: "MFM 도움말"
intro: "MFM는 Misskey의 다양한 곳에서 사용할 수 있는 전용 마크업 언어입니다. 여기에서는 MFM에서 사용할 수 있는 구문을 확인할 수 있습니다."
mention: "멘션"
mentionDescription: "골뱅이표(@) 뒤에 사용자명을 넣어 특정 유저를 나타낼 수 있습니다."
hashtag: "해시태그"
hashtagDescription: "샵 또는 우물정자(#)를 앞에 붙여서 해시태그를 나타낼 수 있습니다."
url: "URL"
urlDescription: "URL을 나타낼 수 있습니다."
link: "링크"
boldDescription: "문자를 굵게 강조합니다."
smallDescription: "내용을 작고 연하게 보이게 합니다."
center: "가운데 정렬"
centerDescription: "내용을 가운데 정렬로 보이게 합니다."
inlineMathDescription: "수식(KaTeX)를 인라인으로 보이게 합니다."
blockMathDescription: "여러 줄의 수식(KaTeX)를 블록으로 보이게 합니다."
quote: "인용"
emoji: "커스텀 이모지"
emojiDescription: "커스텀 이모지의 이름을 쌍점(:)으로 감싸서 커스텀 이모지를 사용합니다."
search: "검색"
searchDescription: "주어진 키워드가 입력된 검색창을 보이게 합니다."
_reversi:
reversi: "리버시"
gameSettings: "대국 설정"
chooseBoard: "보드 선택"
blackOrWhite: "선공/후공"
blackIs: "{name}님이 흑(선공)"
rules: "규칙"
botSettings: "Bot 설정"
thisGameIsStartedSoon: "잠시 후에 대국이 시작됩니다"
waitingForOther: "상대의 준비가 완료될 때까지 기다리고 있습니다"
waitingForMe: "당신의 준비 완료를 기다리고 있습니다"
myTurn: "당신의 차례입니다"
turnOf: "{name}님의 차례입니다"
pastTurnOf: "{name}님의 차례"
surrender: "기권"
surrendered: "기권에 의해"
drawn: "무승부"
won: "{name}님의 승리"
total: "합계"
turnCount: "{count}턴 째"
myGames: "내 대국"
allGames: "모두의 대국"
ended: "종료"
playing: "지금 대국 중"
isLlotheo: "돌이 적은 사람이 승리 (llotheo)"
loopedMap: "루프 지도"
canPutEverywhere: "어디에나 놓을 수 있음"
_instanceTicker:
none: "보이지 않음"
remote: "리모트 유저에게만 보이기"
always: "항상 보이기"
_channel:
create: "채널 생성"
setBanner: "배너 설정"
@@ -568,10 +665,11 @@ _channel:
usersCount: "{n}명 참여 중"
notesCount: "{n}노트"
_sidebar:
icon: "아바타"
icon: "아이콘"
hide: "숨기기"
_wordMute:
muteWords: "뮤트할 단어"
muteWordsDescription: "공백으로 구분하는 경우 AND, 줄바꿈으로 구분하는 경우 OR로 지정됩니다。"
mutedNotes: "뮤트된 노트"
_theme:
explore: "테마 찾아보기"
@@ -943,6 +1041,8 @@ _pages:
id: "캔버스 ID"
width: "폭"
height: "높이"
_note:
detailed: "세부 정보 보기"
switch: "스위치"
_switch:
name: "변수명"

View File

@@ -95,6 +95,7 @@ sensitive: "Содержимое не для всех"
add: "Добавить"
reaction: "Реакции"
reactionSettingDescription: "Подберите, что будет у вас в палитре реакций"
reactionSettingDescription2: "Меняйте порядок перетаскиванием. Удаляйте нажатием."
rememberNoteVisibility: "Запоминать видимость заметок"
attachCancel: "Удалить вложение"
markAsSensitive: "Отметить как «не для всех»"
@@ -124,6 +125,7 @@ settingGuide: "Рекомендуемые настройки"
cacheRemoteFiles: "Кешировать внешние файлы"
cacheRemoteFilesDescription: "Когда эта настройка отключена, файлы с других сайтов будут загружаться прямо оттуда. Это сэкономит место на сервере, но увеличит трафик, так как не будут создаваться эскизы."
flagAsBot: "Аккаунт бота"
flagAsBotDescription: "Включите, если этот аккаунт управляется программой. Это позволит системе Misskey учитывать это, а также поможет разработчикам других ботов предотвратить бесконечные циклы взаимодействия."
flagAsCat: "Аккаунт кота"
autoAcceptFollowed: "Принимать подписчиков автоматически"
addAcount: "Добавить аккаунт"
@@ -205,7 +207,7 @@ newPassword: "Новый пароль"
newPasswordRetype: "Новый пароль (ещё раз)"
attachFile: "Прикрепить файлы"
more: "Ещё!"
featured: "Подборка"
featured: "Горячее"
usernameOrUserId: "Имя или идентификатор пользователя"
noSuchUser: "Таких пользователей не найдено"
lookup: "Запрос"
@@ -315,6 +317,8 @@ bannerUrl: "Ссылка на изображение в шапке"
basicInfo: "Общая информация"
pinnedUsers: "Прикреплённый пользователь"
pinnedUsersDescription: "Перечислите по одному имени пользователя в строке. Пользователи, перечисленные здесь, будут привязаны к закладке \"Изучение\"."
pinnedPages: "Закрепленные страницы"
pinnedPagesDescription: "Если хотите закрепить страницы на главной сайта, сюда можно добавить пути к ним, каждый в отдельной строке."
hcaptcha: "hCaptcha"
enableHcaptcha: "Включить hCaptcha"
hcaptchaSiteKey: "Ключ сайта"
@@ -543,6 +547,11 @@ deck: "Пульт"
undeck: "Покинуть пульт"
useBlurEffectForModal: "Размывка под формой поверх всего"
useFullReactionPicker: "Полнофункциональный выбор реакций"
width: "Ширина"
height: "Высота"
large: "Крупно"
medium: "Средне"
small: "Мелко"
generateAccessToken: "Создать токен доступа"
permission: "Разрешения"
enableAll: "Включить все"
@@ -605,6 +614,11 @@ random: "Случайные"
system: "Система"
switchUi: "Выбор вида"
desktop: "Стол"
clip: "Памятки"
createNew: "Новый документ"
optional: "Необязательно"
createNewClip: "Новая памятка"
public: "Общедоступно"
_mfm:
cheatSheet: "Подсказка по разметке MFM"
intro: "MFM — язык оформления текста, придуманный специально для Misskey, который здесь можно много где использовать. На этой странице собраны и кратко изложены способы его применения."
@@ -699,9 +713,9 @@ _channel:
edit: "Редактировать канал"
setBanner: "Установить баннер"
removeBanner: "Удалить баннер"
featured: "Из подборки"
owned: "Владелец"
following: "Читаю"
featured: "Актуальные"
owned: "Собственные"
following: "Подписки"
usersCount: "Участников: {n}"
notesCount: "Заметок: {n}"
_sidebar:
@@ -815,10 +829,10 @@ _time:
_tutorial:
title: "Как пользоваться Misskey"
step1_1: "Добро пожаловать!"
step1_2: "Эта страница называется «лента». Здесь будут появляться ваши «заметки» и тех, на кого вы «подписаны», и располагаться в порядке времени их появления."
step1_2: "Эта страница называется «лента». Здесь будут появляться «заметки»: ваши личные и тех, на кого вы «подписаны». Они будут располагаться в порядке времени их появления."
step1_3: "Правда, ваша лента пока пуста. Она начнёт заполняться, когда вы будете писать свои заметки и подписываться на других."
step2_1: "Давайте, сначала заполним профиль, прежде чем начать писать заметки и подписываться на других."
step2_2: "То, что вы расскажете в профиле, поможет многим лучше вас узнать, а значит, им будет легче присоединиться к вам — подписаться и читать заметки."
step2_1: "Давайте, заполним профиль, прежде чем начать писать заметки и подписываться на других."
step2_2: "То, что вы расскажете в профиле, поможет лучше вас узнать, а значит, многим будет легче присоединиться — вы скорее получите новых подписчиков и читателей."
step3_1: "Успешно заполнили профиль?"
step3_2: "Что ж, теперь самое время опубликуовать заметку. Если нажать вверху страницы на изображение карандаша, появится форма для текста."
step3_3: "Напишите в неё, что хотите, и нажмите на кнопку в правом верхнем углу."
@@ -826,11 +840,11 @@ _tutorial:
step4_1: "С написанием первой заметки покончено?"
step4_2: "Отлично, теперь она должна появиться в вашей ленте."
step5_1: "А теперь самое время немного оживить ленту, подписавшись на других."
step5_2: "На странице «{featured}» собраны популярные сегодня заметки, читая которые, вы можете найти кого-то вам интересного, а на «{explore}» можно посмотреть, кто популярен у остальных."
step5_2: "На странице «{featured}» собраны популярные сегодня заметки, читая которые, вы можете найти кого-то вам интересного, а на странице «{explore}» можно посмотреть, кто популярен у остальных."
step5_3: "Чтобы подписаться на кого-нибудь, щёлкните по его аватару и в открывшемся профиле нажмите кнопку «Подписаться»."
step5_4: "Некоторые пользователи (около их имени «висит замок») вручную подтверждают чужие подписки. Так что иногда подписка начинает работать не сразу.\n"
step6_1: "Если теперь в ленте видны и чужие заметки, значит у вас получилось."
step6_2: "Можете ставить «реакции» чужим заметкам, чтобы непринуждённо выразить свои чувства к ним."
step6_2: "Здесь можно непринуждённо выразить свои чувства к чьей-то заметке, отметив «реакцию» под ней."
step6_3: "Отмечайте реакции, нажмая на символ «+» под заметкой и выбирая значок по душе."
step7_1: "На этом вводный урок по использованию Misskey закончен. Спасибо, что прошли его до конца!"
step7_2: "Хотите изучить Misskey глубже — добро пожаловать в раздел «{help}»."
@@ -1070,6 +1084,7 @@ _pages:
created: "Страница успешно создана."
updated: "Страница успешно обновлена."
deleted: "Страница успешно удалена."
pageSetting: "Настройки страницы"
nameAlreadyExists: "Указанный адрес страницы уже существует."
invalidNameTitle: "Указанный адрес страницы недопустим."
invalidNameText: "Проверьте, что не оставили поле пустым."
@@ -1080,6 +1095,7 @@ _pages:
unlike: "Отменить «нравится»"
my: "Свои страницы"
liked: "Понравившиеся страницы"
featured: "Популярные"
inspector: "Инспектор"
contents: "Содержательные"
content: "Содержимое"
@@ -1135,6 +1151,11 @@ _pages:
id: "Метка холста"
width: "Ширина"
height: "Высота"
note: "Встроенная заметка"
_note:
id: "Идентификатор заметки"
idDescription: "Можно также вставить ссылку на заметку."
detailed: "Подробный вид"
switch: "Выключатель"
_switch:
name: "Имя переменной"

View File

@@ -1,5 +1,6 @@
---
_lang_: "Українська"
introMisskey: "Ласкаво просимо! Misskey - децентралізована служба мікроблогів з відкритим кодом.\nСтворюйте \"нотатки\", щоб поділитися тим, що відбувається, і розповісти всім про себе 📡\nЗа допомогою \"реакцій\" ви також можете швидко висловити свої почуття щодо нотаток інших 👍\nДавайте досліджувати новий світ 🚀"
monthAndDay: "{month}/{day}"
search: "Пошук"
notifications: "Сповіщення"
@@ -11,7 +12,7 @@ gotIt: "Зрозуміло!"
cancel: "Скасувати"
enterUsername: "Введіть ім'я користувача"
renotedBy: "Поширено {user}"
noNotes: "Немає дописів"
noNotes: "Немає нотаток"
noNotifications: "Немає сповіщень"
instance: "Інстанс"
settings: "Налаштування"
@@ -38,13 +39,14 @@ copyContent: "Скопіювати контент"
copyLink: "Скопіювати посилання"
delete: "Видалити"
deleteAndEdit: "Видалити й редагувати"
deleteAndEditConfirm: "Ви впевнені, що хочете видалити цю нотатку та відредагувати її? Ви втратите всі реакції, поширення та відповіді на неї."
addToList: "Додати до списку"
sendMessage: "Надіслати повідомлення"
copyUsername: "Скопіювати ім’я користувача"
searchUser: "Пошук користувачів"
reply: "Відповісти"
loadMore: "Показати більше"
youGotNewFollower: "У вас новий підписник"
youGotNewFollower: "Новий підписник"
receiveFollowRequest: "Отримано запит на підписку"
followRequestAccepted: "Запит на підписку прийнято"
mention: "Згадка"
@@ -55,11 +57,14 @@ import: "Імпорт"
export: "Експорт"
files: "Файли"
download: "Завантажити"
driveFileDeleteConfirm: "Ви впевнені, що хочете видалити файл {name}? Нотатки із цим файлом також буде видалено."
unfollowConfirm: "Ви впевнені, що хочете відписатися від {name}?"
exportRequested: "Ви запросили експорт. Це може зайняти деякий час. Після завершення експорту отриманий файл буде додано на диск."
importRequested: "Ви запросили імпорт. Це може зайняти деякий час."
lists: "Списки"
noLists: "Немає списків"
note: "Дописи"
notes: "Дописи"
note: "Нотатки"
notes: "Нотатки"
following: "Підписки"
followers: "Підписники"
followsYou: "Підписаний(-а) на вас"
@@ -69,10 +74,11 @@ error: "Помилка"
somethingHappened: "Щось пішло не так"
retry: "Спробувати знову"
pageLoadError: "Помилка при завантаженні сторінки"
pageLoadErrorDescription: "Зазвичай це пов’язано з помилками мережі або кешем браузера. Очистіть кеш або почекайте трохи й спробуйте ще раз."
enterListName: "Введіть назву списку"
privacy: "Приватність"
makeFollowManuallyApprove: "Підтверджувати підписників уручну"
defaultNoteVisibility: "Видимість допису за замовчуванням"
defaultNoteVisibility: "Видимість за замовчуванням"
follow: "Підписка"
followRequest: "Запит на підписку"
followRequests: "Запити на підписку"
@@ -82,13 +88,15 @@ enterEmoji: "Введіть емодзі"
renote: "Поширити"
unrenote: "Відміна поширення"
quote: "Цитата"
pinnedNote: "Закріплений допис"
pinnedNote: "Закріплена нотатка"
you: "Ви"
clickToShow: "Натисніть для перегляду"
sensitive: "NSFW"
add: "Додати"
reaction: "Реакції"
rememberNoteVisibility: "Пам’ятати видимість дописів"
reactionSettingDescription: "Виберіть свої улюблені реакції, які хочете закріпити в селекторі реакцій."
reactionSettingDescription2: "Перетягніть для реорганізації, натисніть для видалення."
rememberNoteVisibility: "Пам’ятати параметри видимісті"
attachCancel: "Видалити вкладення"
markAsSensitive: "Позначити як NSFW"
unmarkAsSensitive: "Зняти позначку NSFW"
@@ -115,6 +123,7 @@ emojiUrl: "URL емодзі"
addEmoji: "Додати емодзі"
settingGuide: "Рекомендована конфігурація"
cacheRemoteFiles: "Кешувати дані з інших інстансів"
cacheRemoteFilesDescription: "Якщо кешування вимкнено, віддалені файли завантажуються безпосередньо з віддаленого інстансу. Це зменшує використання сховища, але збільшує трафік, оскільки не генеруются ескізи."
flagAsBot: "Акаунт бота"
flagAsCat: "Акаунт кота"
autoAcceptFollowed: "Автоматично приймати запити на підписку від користувачів, на яких ви підписані"
@@ -141,7 +150,7 @@ latestRequestReceivedAt: "Останній запит прийнято"
latestStatus: "Останній статус"
storageUsage: "Використання простіру"
charts: "Графіки"
perHour: "Щогодини"
perHour: "Щогодинно"
perDay: "Щоденно"
stopActivityDelivery: "Припинити розсилання активності"
blockThisInstance: "Заблокувати цей інстанс"
@@ -159,16 +168,19 @@ instanceInfo: "Про цей інстанс"
statistics: "Статистика"
clearQueue: "Очистити чергу"
clearQueueConfirmTitle: "Ви впевнені, що хочете очистити чергу?"
clearQueueConfirmText: "Будь-які невідправлені нотатки, що залишилися в черзі, не будуть передані. Зазвичай ця операція НЕ потрібна."
clearCachedFiles: "Очистити кеш"
clearCachedFilesConfirm: "Ви впевнені, що хочете видалити всі кешовані файли?"
blockedInstances: "Заблоковані інстанси"
blockedInstancesDescription: "Вкажіть інстанси, які потрібно заблокувати. Перелічені інстанси більше не зможуть спілкуватися з цим інстансом."
muteAndBlock: "Ігнор і блокування"
mutedUsers: "Ігноровані користувачі"
blockedUsers: "Заблоковані користувачі"
noUsers: "Немає користувачів"
editProfile: "Редагувати профіль"
noteDeleteConfirm: "Ви дійсно хочете видалити цей допис?"
pinLimitExceeded: "Більше дописів не можна закріпити"
noteDeleteConfirm: "Ви дійсно хочете видалити цю нотатку?"
pinLimitExceeded: "Більше нотаток не можна закріпити"
intro: "Встановлення Misskey завершено! Будь ласка, створіть акаунт адміністратора."
done: "Готово"
processing: "Обробка"
preview: "Передогляд"
@@ -178,32 +190,52 @@ noJobs: "Немає завдань"
federating: "Федерується"
blocked: "Заблоковано"
suspended: "Призупинено"
all: "Всі"
subscribing: "Підписка"
publishing: "Публікація"
notResponding: "Не відповідає"
instanceFollowing: "Підписка на інстанс"
instanceFollowers: "Підписники інстансу"
instanceUsers: "Користувачі цього інстансу"
changePassword: "Змінити пароль"
security: "Безпека"
retypedNotMatch: "Введені дані не збігаються."
currentPassword: "Поточний пароль"
newPassword: "Новий пароль"
newPasswordRetype: "Новий пароль (повторно)"
attachFile: "Вкласти файл"
more: "Бiльше!"
featured: "Виділено"
featured: "Популярні"
usernameOrUserId: "Ім'я або ID користувача"
noSuchUser: "Користувача не знайдено"
lookup: "Пошук"
announcements: "Оголошення"
imageUrl: "URL зображення"
remove: "Видалити"
removed: "Видалено"
removeAreYouSure: "Ви впевнені, що хочете видалити \"{x}\"?"
deleteAreYouSure: "Ви впевнені, що хочете видалити \"{x}\"?"
resetAreYouSure: "Дійсно ресет?"
saved: "Збережено"
messaging: "Чати"
upload: "Завантажити"
fromDrive: "З диска"
fromUrl: "З URL"
uploadFromUrl: "Завантажити з URL"
uploadFromUrlDescription: "URL-адреса файлу для завантаження"
uploadFromUrlRequested: "Обрано завантаження"
uploadFromUrlMayTakeTime: "Завантаження може зайняти деякий час."
explore: "Огляд"
games: "Ігри Misskey"
messageRead: "Прочитано"
noMoreHistory: "Подальшої історії немає"
startMessaging: "Розпочати діалог"
nUsersRead: "Прочитали {n}"
agreeTo: "Я погоджуюсь з {0}"
tos: "Умови використання"
start: "Розпочати"
home: "Домівка"
remoteUserCaution: "Інформація може бути неповною, оскільки це віддалений користувач."
activity: "Активність"
images: "Зображення"
birthday: "День народження"
@@ -217,6 +249,7 @@ light: "Світла"
dark: "Темна"
lightThemes: "Світлі теми"
darkThemes: "Темні теми"
syncDeviceDarkMode: "Синхронізувати темний режим із налаштуваннями вашого пристрою"
drive: "Диск"
fileName: "Ім'я файлу"
selectFile: "Вибрати файл"
@@ -234,13 +267,15 @@ emptyFolder: "Тека порожня"
unableToDelete: "Видалення неможливе"
inputNewFileName: "Введіть ім'я нового файлу"
inputNewFolderName: "Введіть ім'я нової теки"
circularReferenceFolder: "Ви намагаєтесь перемістити папку в її підпапку."
hasChildFilesOrFolders: "Ця тека не порожня і не може бути видалена"
copyUrl: "Копіювати URL"
rename: "Перейменувати"
avatar: "Аватар"
banner: "Банер"
nsfw: "NSFW"
disconnectedFromServer: "Підключення до сервера було перервано"
whenServerDisconnected: "Коли зв’язок із сервером втрачено"
disconnectedFromServer: "Зв’язок із сервером було перервано"
reload: "Оновити"
doNothing: "Нічого не робити"
reloadConfirm: "Перезавантажити стрічку?"
@@ -261,18 +296,24 @@ monthX: "{month}"
yearX: "{year}"
pages: "Сторінки"
integration: "Інтеграція"
connectSerice: "Під’єднатися"
connectSerice: "Під’єднати"
disconnectSerice: "Відключитися"
enableLocalTimeline: "Увімкнути локальну стрічку"
enableGlobalTimeline: "Увімкнути глобальну стрічку"
disablingTimelinesInfo: "Адміністратори та модератори завжди мають доступ до всіх стрічок, навіть якщо вони вимкнуті."
registration: "Реєстрація"
enableRegistration: "Дозволити реєстрацію"
invite: "Запрошення"
invite: "Запросити"
proxyRemoteFiles: "Проксувати файли з інших інстансів"
driveCapacityPerLocalAccount: "Об'єм диска на одного локального користувача"
driveCapacityPerRemoteAccount: "Об'єм диска на одного віддаленого користувача"
inMb: "В мегабайтах"
iconUrl: "URL аватара"
bannerUrl: "URL банера"
basicInfo: "Основна інформація"
pinnedUsers: "Закріплені користувачі"
pinnedPages: "Закріплені сторінки"
pinnedPagesDescription: "Введіть шляхи сторінок, які ви бажаєте закріпити на головній сторінці цього інстанса, розділені новими рядками."
hcaptcha: "hCaptcha"
enableHcaptcha: "Увімкнути hCaptcha"
hcaptchaSiteKey: "Ключ сайту"
@@ -287,10 +328,17 @@ name: "Ім'я"
antennaSource: "Джерело антени"
antennaKeywords: "Ключові слова антени"
antennaExcludeKeywords: "Винятки"
notifyAntenna: "Сповіщати про нові нотатки"
withFileAntenna: "Тільки нотатки з вкладеними файлами"
serviceworker: "ServiceWorker"
enableServiceworker: "Ввімкнути ServiceWorker"
caseSensitive: "З урахуванням регістру"
notesAndReplies: "Дописи та відповіді"
withReplies: "Включаючи відповіді"
connectedTo: "Наступні акаунти під'єднані"
notesAndReplies: "Нотатки та відповіді"
withFiles: "Файли"
silence: "Тиша"
unsilence: "Зняти тишу"
popularUsers: "Популярні користувачі"
recentlyUpdatedUsers: "Нещодавно активні користувачі"
recentlyRegisteredUsers: "Нещодавно зареєстровані користувачі"
@@ -323,7 +371,7 @@ group: "Група"
groups: "Групи"
createGroup: "Створити групу"
ownedGroups: "Власні групи"
invites: "Запрошення"
invites: "Запросити"
groupName: "Назва групи"
transfer: "Передача"
messagingWithUser: "Чат з користувачами"
@@ -332,9 +380,9 @@ title: "Тема"
text: "Текст"
next: "Далі"
retype: "Введіть ще раз"
noteOf: "Допис {user}"
noteOf: "Нотатка {user}"
inviteToGroup: "Запрошення до групи"
maxNoteTextLength: "Максимальна довжина допису"
maxNoteTextLength: "Максимальна довжина нотатки"
quoteAttached: "Цитата"
quoteQuestion: "Ви хочете додати цитату?"
noMessagesYet: "Ще немає повідомлень"
@@ -371,15 +419,18 @@ fontSize: "Розмір шрифту"
noFollowRequests: "Немає запитів на підписку"
dashboard: "Панель приладів"
local: "Локальні"
remote: "Віддалений"
remote: "Віддалені"
total: "Всього"
weekOverWeekChanges: "Тиждень"
dayOverDayChanges: "Доба"
appearance: "Вигляд"
clientSettings: "Налаштування клієнта"
accountSettings: "Налаштування акаунта"
promotion: "Просування"
promote: "Просунути"
promotion: "Виділене"
promote: "Виділити"
numberOfDays: "Кількість днів"
hideThisNote: "Сховати цей допис"
hideThisNote: "Сховати цю нотатку"
showFeaturedNotesInTimeline: "Показувати популярні нотатки у стрічці"
objectStorageBaseUrl: "Base URL"
objectStorageBucket: "Bucket"
objectStoragePrefix: "Prefix"
@@ -388,47 +439,216 @@ objectStorageRegion: "Region"
objectStorageUseSSL: "Використовувати SSL"
objectStorageUseProxy: "Використовувати Proxy"
deleteAll: "Видалити все"
newNoteRecived: "Є нові дописи"
sounds: "Звук"
newNoteRecived: "Є нові нотатки"
sounds: "Звуки"
listen: "Слухати"
none: "Відсутній"
showInPage: "Показати на сторінці"
popout: "Від'єднати"
volume: "Гучність"
install: "Інсталювати"
details: "Детальніше"
chooseEmoji: "Виберіть емодзі"
recentUsed: "Нещодавні"
install: "Встановити"
uninstall: "Видалити"
installedApps: "Встановлені аплікації"
nothing: "Тут нічого немає"
installedDate: "Дата встановлення"
lastUsedDate: "Дата використання"
state: "Стан"
sort: "Сортування"
ascendingOrder: "За зростанням"
descendingOrder: "За спаданням"
scratchpad: "Чернетка"
output: "Вихід"
script: "Скрипт"
deleteAllFiles: "Видалити всі файли"
deleteAllFilesConfirm: "Ви дійсно хочете видалити всі файли?"
removeAllFollowing: "Скасувати всі підписки"
sidebar: "Бокова панель"
addItem: "Додати елемент"
rooms: "Кімнати"
relays: "Ретранслятори"
addRelay: "Додати ретранслятор"
addedRelays: "Додані ретранслятори"
deletedNote: "Видалена нотатка"
visibility: "Видимість"
poll: "Опитування"
expandTweet: "Розгорнути твіт"
themeEditor: "Редактор тем"
description: "Опис"
author: "Автор"
manage: "Управління"
plugins: "Плагіни"
large: "Крупний"
medium: "Середній"
small: "Маленький"
generateAccessToken: "Згенерувати токен доступу"
permission: "Права"
enableAll: "Ввімкути все"
disableAll: "Вимкнути все"
tokenRequested: "Надати доступ до акаунту"
notificationType: "Тип сповіщення"
edit: "Редагувати"
useStarForReactionFallback: "Використовувати ★ як запасний варіант, якщо емодзі реакції невідомий"
emailConfig: "Налаштування email сервера"
email: "E-mail адреса"
smtpHost: "Хост"
smtpPort: "Порт"
smtpUser: "Ім'я користувача"
smtpPass: "Пароль"
testEmail: "Тестовий email"
wordMute: "Ігнор слів"
makeActive: "Активувати"
copy: "Скопіювати"
metrics: "Показники"
overview: "Огляд"
logs: "Журнал"
delayed: "Затримка"
database: "База даних"
channel: "Канали"
create: "Створити"
notificationSetting: "Параметри сповіщень"
notificationSettingDesc: "Виберіть типи сповіщень для відображення"
useGlobalSetting: "Застосувати глобальнi параметри"
other: "Інше"
regenerateLoginToken: "Оновити Login Token"
sample: "Приклад"
reportAbuse: "Поскаржитись"
reportAbuseOf: "Поскаржитись на {name}"
send: "Відправити"
abuseMarkAsResolved: "Позначити скаргу як вирішену"
openInNewTab: "Відкрити в новій вкладці"
waitingFor: "Чекаємо на {x}"
random: "Випадковий"
system: "Система"
switchUi: "Інтерфейс"
desktop: "Десктоп"
clip: "Добірка"
public: "Публічний"
_mfm:
cheatSheet: " Довідка MFM"
intro: "MFM це ексклюзивна мова розмітки тексту в Misskey, яку можна використовувати в багатьох місцях. Тут ви можете переглянути приклади її синтаксису."
mention: "Згадка"
hashtag: "Хештеґ"
url: "URL"
link: "Посилання"
linkDescription: "Окремі частини тексту можуть містити посилання"
bold: "Жирний шрифт"
boldDescription: "Виділяє літери, роблячи їх товще"
small: "Дрібний шрифт"
smallDescription: "Робить текст маленьким і тонким"
center: "По центру"
centerDescription: "Показує вміст у центрі"
inlineCode: "Код (у рядку)"
inlineCodeDescription: "Показує фрагмент тексту у рядку як програмний код"
blockCode: "Код (блок)"
blockCodeDescription: "Показує кілька рядків тексту як блок програмного кода"
inlineMath: "Формула (у рядку)"
blockMath: "Формули (блок)"
quote: "Цитата"
emoji: "Кастомні емоджі"
search: "Пошук"
searchDescription: "Відображає вікно пошуку з попередньо введеним текстом"
flipDescription: "Віддзеркалює вміст по горизонталі або вертикалі"
jelly: "Анімація (желе)"
jellyDescription: "Створює желеподібну анімацію"
tada: "Анімація (Тада!)"
tadaDescription: "Створює анімацію з відчуттям \"Тада!\""
jump: "Анімація (стрибки)"
jumpDescription: "Показує стрибаючу анімацію"
bounce: "Анімація (пружина)"
shake: "Анімація (Shake)"
twitch: "Анімація (Twitch)"
spin: "Анімація (Spin)"
_reversi:
reversi: "Реверсі"
gameSettings: "Налаштування гри"
chooseBoard: "Вибір дошки"
blackOrWhite: "Чорні / Білі"
blackIs: "{name} грає чорними"
rules: "Правила"
thisGameIsStartedSoon: "Гра розпочнеться через кілька секунд"
waitingForOther: "Чекаємо на хід суперника"
waitingForMe: "Чекаємо на ваш хід"
waitingBoth: "Приготуйтесь"
ready: "Готовність"
cancelReady: "Скасувати готовність"
opponentTurn: "Хід суперника"
myTurn: "Ваш хід"
turnOf: "Хід {name}"
pastTurnOf: "Хід {name}"
surrender: "Здатися"
drawn: "Нічия"
won: "Перемога {name}"
black: "Чорні"
white: "Білі"
total: "Всього"
turnCount: "Хід {count}"
myGames: "Мої ігри"
allGames: "Усі ігри"
ended: "Завершено"
_instanceTicker:
none: "Не відображати"
remote: "Відображати для віддалених користувачів"
always: "Відображати завжди"
_serverDisconnectedBehavior:
reload: "Автоматично перезавантажити"
dialog: "Показати діалогове вікно"
quiet: "Показати ненав’язливе попередження"
_channel:
create: "Створити канал"
edit: "Редагувати канал"
setBanner: "Встановити банер"
removeBanner: "Видалити банер"
featured: "Тренди"
_sidebar:
icon: "Аватар"
_theme:
keys:
accent: "Акцент"
bg: "Фон"
fg: "Текст"
focus: "Фокус"
indicator: "Індикатор"
panel: "Панель"
shadow: "Тінь"
header: "Заголовок"
navBg: "Фон бокової панелі"
navFg: "Текст бокової панелі"
navHoverFg: "Текст бокової панелі (під курсором)"
navActive: "Текст бокової панелі (активне)"
navIndicator: "Індикатор бокової панелі"
link: "Посилання"
hashtag: "Хештеґ"
mention: "Згадка"
mentionMe: "Згадки (мене)"
renote: "Поширити"
_sfx:
note: "Дописи"
note: "Нотатки"
notification: "Сповіщення"
chat: "Чати"
_ago:
unknown: "Невідомо"
future: "Майбутнє"
justNow: "Щойно"
secondsAgo: "{n}с тому"
minutesAgo: "{n}х тому"
hoursAgo: "{n}г тому"
daysAgo: "{n}д тому"
weeksAgo: "{n} тиж. тому"
monthsAgo: "{n} міс. тому"
yearsAgo: "{n} р. тому"
_time:
second: "с"
minute: "х"
hour: "г"
day: "д"
_tutorial:
title: "Як користуватись Misskey"
step1_1: "Ласкаво просимо!"
_antennaSources:
homeTimeline: "Дописи тих, на кого ви підписані"
homeTimeline: "Нотатки тих, на кого ви підписані"
_widgets:
notifications: "Сповіщення"
timeline: "Стрічка"
@@ -436,12 +656,14 @@ _widgets:
federation: "Федіверс"
_cw:
show: "Показати більше"
_poll:
deadlineTime: "г"
_visibility:
home: "Домівка"
followers: "Підписники"
localOnly: "Лише локально"
_postForm:
replyPlaceholder: "Відповідь на допис..."
replyPlaceholder: "Відповідь на цю нотатку..."
_profile:
name: "Ім'я"
username: "Ім'я користувача"
@@ -458,6 +680,7 @@ _rooms:
_furnitures:
monitor: "Монітор"
_pages:
featured: "Популярні"
blocks:
image: "Зображення"
script:
@@ -476,11 +699,17 @@ _pages:
arg1: "Списки"
_listLen:
arg1: "Списки"
_fn:
arg1: "Вихід"
types:
array: "Списки"
_relayStatus:
requesting: "Очікує затвердження"
accepted: "Затверджено"
rejected: "Відхилено"
_notification:
youRenoted: "{name} поширив(ла) ваш допис"
youWereFollowed: "У вас новий підписник"
youRenoted: "{name} поширив(ла) вашу нотатку"
youWereFollowed: "Новий підписник"
_types:
follow: "Підписки"
mention: "Згадка"

View File

@@ -95,6 +95,7 @@ sensitive: "阅读注意"
add: "添加"
reaction: "回应"
reactionSettingDescription: "选择您想要置顶的回应。"
reactionSettingDescription2: "通过拖动来重新排列。单击即可删除。"
rememberNoteVisibility: "记录公开范围"
attachCancel: "删除附件"
markAsSensitive: "阅读注意"
@@ -124,6 +125,7 @@ settingGuide: "推荐配置"
cacheRemoteFiles: "远程文件缓存"
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程实例载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
flagAsBot: "这个账户是Bot"
flagAsBotDescription: "如果此帐户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此帐户识别为机器人。"
flagAsCat: "这个账户是Cat"
autoAcceptFollowed: "自动允许关注"
addAcount: "添加账户"
@@ -315,6 +317,8 @@ bannerUrl: "Banner URL"
basicInfo: "基本信息"
pinnedUsers: "置顶用户"
pinnedUsersDescription: "在「发现」页面中使用换行标记想要置顶的用户。"
pinnedPages: "固定页面"
pinnedPagesDescription: "输入您要固定到实例首页的页面路径,以换行符分隔。"
hcaptcha: "hCaptcha"
enableHcaptcha: "启用 hCaptcha"
hcaptchaSiteKey: "网站密钥"
@@ -543,6 +547,11 @@ deck: "Deck"
undeck: "取消Deck"
useBlurEffectForModal: "模态框使用模糊效果"
useFullReactionPicker: "使用全功能的回应工具栏"
width: "宽度"
height: "高度"
large: "大"
medium: "中"
small: "小"
generateAccessToken: "生成访问令牌"
permission: "权限"
enableAll: "启用全部"
@@ -605,6 +614,11 @@ random: "随机"
system: "系统"
switchUi: "切换界面"
desktop: "桌面"
clip: "片段"
createNew: "新建"
optional: "可选"
createNewClip: "新建片段"
public: "公开"
_mfm:
cheatSheet: "MFM代码速查表"
intro: "MFM是一种在Misskey中的各个位置使用的专用标记语言。在这里您可以看到MFM中可用的语法列表。"
@@ -1070,6 +1084,7 @@ _pages:
created: "页面已创建"
updated: "页面已更新"
deleted: "该页面已被删除"
pageSetting: "页面设置"
nameAlreadyExists: "该页面URL已存在"
invalidNameTitle: "无效的页面URL"
invalidNameText: "请确认该项不为空"
@@ -1080,6 +1095,7 @@ _pages:
unlike: "取消赞"
my: "我的页面"
liked: "喜欢的页面"
featured: "热门"
inspector: "检查器"
contents: "内容"
content: "页面内容"
@@ -1135,6 +1151,11 @@ _pages:
id: "画布ID"
width: "宽度"
height: "高度"
note: "嵌入的帖子"
_note:
id: "帖子ID"
idDescription: "您也可以通过粘贴帖子的URL来进行设置。"
detailed: "显示详细信息"
switch: "开关"
_switch:
name: "变量名"

View File

@@ -519,6 +519,8 @@ plugins: "插件"
pluginInstallWarn: "請不要安裝來源不明的插件。"
deck: "多欄模式"
undeck: "取消多欄模式"
width: "寬度"
height: "高度"
permission: "權限"
enableAll: "啟用全部"
disableAll: "停用全部"
@@ -558,6 +560,7 @@ send: "發送"
openInNewTab: "在新分頁中開啟"
random: "隨機"
system: "系統"
public: "公開"
_mfm:
mention: "提及"
hashtag: "#tag"

View File

@@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class instancePinnedPages1605585339718 implements MigrationInterface {
name = 'instancePinnedPages1605585339718'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" ADD "pinnedPages" character varying(512) array NOT NULL DEFAULT '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}'::varchar[]`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "pinnedPages"`);
}
}

View File

@@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class instanceImages1605965516823 implements MigrationInterface {
name = 'instanceImages1605965516823'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" ADD "backgroundImageUrl" character varying(512)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "logoImageUrl" character varying(512)`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "logoImageUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "backgroundImageUrl"`);
}
}

View File

@@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class noCrawle1606191203881 implements MigrationInterface {
name = 'noCrawle1606191203881'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "noCrawle" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."noCrawle" IS 'Whether reject index by crawler.'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`COMMENT ON COLUMN "user_profile"."noCrawle" IS 'Whether reject index by crawler.'`);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "noCrawle"`);
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "misskey",
"author": "syuilo <syuilotan@yahoo.co.jp>",
"version": "12.57.4",
"version": "12.60.0",
"codename": "indigo",
"repository": {
"type": "git",
@@ -242,7 +242,7 @@
"vue": "3.0.2",
"vue-color": "2.7.1",
"vue-draggable-next": "1.0.8",
"vue-i18n": "9.0.0-beta.6",
"vue-i18n": "9.0.0-beta.7",
"vue-json-pretty": "1.7.1",
"vue-loader": "16.0.0-beta.8",
"vue-prism-editor": "1.2.2",
@@ -252,7 +252,7 @@
"vuex": "4.0.0-rc.1",
"vuex-persistedstate": "3.1.0",
"web-push": "3.4.4",
"webpack": "5.4.0",
"webpack": "5.6.0",
"webpack-cli": "4.2.0",
"websocket": "1.0.32",
"ws": "7.3.1",

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,34 @@
// 常にメモリにロードしておく必要がないような設定情報を保管するストレージ
const PREFIX = 'miux:';
export const defaultDeviceSettings = {
sound_masterVolume: 0.3,
sound_note: { type: 'syuilo/down', volume: 1 },
sound_noteMy: { type: 'syuilo/up', volume: 1 },
sound_notification: { type: 'syuilo/pope2', volume: 1 },
sound_chat: { type: 'syuilo/pope1', volume: 1 },
sound_chatBg: { type: 'syuilo/waon', volume: 1 },
sound_antenna: { type: 'syuilo/triple', volume: 1 },
sound_channel: { type: 'syuilo/square-pico', volume: 1 },
sound_reversiPutBlack: { type: 'syuilo/kick', volume: 0.3 },
sound_reversiPutWhite: { type: 'syuilo/snare', volume: 0.3 },
};
export const device = {
get<T extends keyof typeof defaultDeviceSettings>(key: T): typeof defaultDeviceSettings[T] {
// TODO: indexedDBにする
// ただしその際はnullチェックではなくキー存在チェックにしないとダメ
// (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
const value = localStorage.getItem(PREFIX + key);
if (value == null) {
return defaultDeviceSettings[key];
} else {
return JSON.parse(value);
}
},
set(key: keyof typeof defaultDeviceSettings, value: any): any {
localStorage.setItem(PREFIX + key, JSON.stringify(value));
},
};

View File

@@ -1,6 +1,6 @@
<template>
<MkModal ref="modal" :src="src" @click="$refs.modal.close()" @closed="$emit('closed')">
<div class="omfetrab _popup" :class="{ compact }">
<div class="omfetrab _popup" :class="['w' + width, 'h' + height, { big }]">
<input ref="search" class="search" :class="{ filled: q != null && q != '' }" v-model.trim="q" :placeholder="$t('search')" @paste.stop="paste" @keyup.enter="done()">
<div class="emojis">
<section class="result">
@@ -99,6 +99,7 @@ import { faHeart, faFlag, faLaugh } from '@fortawesome/free-regular-svg-icons';
import MkModal from '@/components/ui/modal.vue';
import Particle from '@/components/particle.vue';
import * as os from '@/os';
import { isDeviceTouch } from '../scripts/is-device-touch';
export default defineComponent({
components: {
@@ -113,7 +114,7 @@ export default defineComponent({
required: false,
default: true
},
compact: {
asReactionPicker: {
required: false
},
},
@@ -125,6 +126,9 @@ export default defineComponent({
emojilist: markRaw(emojilist),
getStaticImageUrl,
pinned: this.$store.state.settings.reactions,
width: this.asReactionPicker ? this.$store.state.device.reactionPickerWidth : 3,
height: this.asReactionPicker ? this.$store.state.device.reactionPickerHeight : 2,
big: this.asReactionPicker ? isDeviceTouch : false,
customEmojiCategories: this.$store.getters['instance/emojiCategories'],
customEmojis: this.$store.state.instance.meta.emojis,
visibleCategories: {},
@@ -315,8 +319,7 @@ export default defineComponent({
},
mounted() {
const isIos = navigator.userAgent.includes('WebKit') && !navigator.userAgent.includes('Chrome');
if (!isIos) {
if (!os.isMobile) {
this.$refs.search.focus({
preventScroll: true
});
@@ -386,18 +389,39 @@ export default defineComponent({
<style lang="scss" scoped>
.omfetrab {
$eachSize: 40px;
$pad: 8px;
--eachSize: 40px;
display: flex;
flex-direction: column;
width: ($eachSize * 7) + ($pad * 2);
contain: content;
--height: 300px;
&.compact {
width: ($eachSize * 5) + ($pad * 2);
--height: 210px;
&.big {
--eachSize: 44px;
}
&.w1 {
width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
}
&.w2 {
width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
}
&.w3 {
width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
}
&.h1 {
--height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
}
&.h2 {
--height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
}
&.h3 {
--height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
}
> .search {
@@ -461,8 +485,8 @@ export default defineComponent({
> button {
position: relative;
padding: 0;
width: $eachSize;
height: $eachSize;
width: var(--eachSize);
height: var(--eachSize);
border-radius: 4px;
&:focus {

View File

@@ -1,6 +1,6 @@
<template>
<XModalWindow ref="dialog"
:width="400"
:width="450"
:can-close="false"
:with-ok-button="true"
:ok-button-disabled="false"
@@ -12,42 +12,61 @@
<template #header>
{{ title }}
</template>
<div class="xkpnjxcv _section">
<label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item">
<MkInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1">
<FormBase class="xkpnjxcv">
<template v-for="item in Object.keys(form).filter(item => !form[item].hidden)">
<FormInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</MkInput>
<MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text">
</FormInput>
<FormInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</MkInput>
<MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]">
</FormInput>
<FormTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]">
<span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</MkTextarea>
<MkSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">
</FormTextarea>
<FormSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]">
<span v-text="form[item].label || item"></span>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</MkSwitch>
</label>
</div>
</FormSwitch>
<FormSelect v-else-if="form[item].type === 'enum'" v-model:value="values[item]">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span></template>
<option v-for="item in form[item].enum" :value="item.value" :key="item.value">{{ item.label }}</option>
</FormSelect>
<FormRange v-else-if="form[item].type === 'range'" v-model:value="values[item]" :min="form[item].mim" :max="form[item].max" :step="form[item].step">
<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span></template>
<template v-if="form[item].description" #desc>{{ form[item].description }}</template>
</FormRange>
<FormButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)">
<span v-text="form[item].content || item"></span>
</FormButton>
</template>
</FormBase>
</XModalWindow>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import XModalWindow from '@/components/ui/modal-window.vue';
import MkInput from './ui/input.vue';
import MkTextarea from './ui/textarea.vue';
import MkSwitch from './ui/switch.vue';
import FormBase from './form/base.vue';
import FormInput from './form/input.vue';
import FormTextarea from './form/textarea.vue';
import FormSwitch from './form/switch.vue';
import FormSelect from './form/select.vue';
import FormRange from './form/range.vue';
import FormButton from './form/button.vue';
export default defineComponent({
components: {
XModalWindow,
MkInput,
MkTextarea,
MkSwitch,
FormBase,
FormInput,
FormTextarea,
FormSwitch,
FormSelect,
FormRange,
FormButton,
},
props: {
@@ -95,12 +114,6 @@ export default defineComponent({
<style lang="scss" scoped>
.xkpnjxcv {
> label {
display: block;
&:not(:last-child) {
margin-bottom: 32px;
}
}
}
</style>

View File

@@ -0,0 +1,56 @@
<template>
<div class="rbusrurv" :class="{ wide: forceWide }" v-size="{ max: [400] }">
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
forceWide: {
type: Boolean,
required: false,
default: false,
}
}
});
</script>
<style lang="scss" scoped>
.rbusrurv {
line-height: 1.4em;
background: var(--bg);
padding: 32px;
&:not(.wide).max-width_400px {
padding: 32px 0;
> ::v-deep(*) {
._formPanel {
border: solid 0.5px var(--divider);
border-radius: 0;
border-left: none;
border-right: none;
}
._form_group {
> * {
&:not(:first-child) {
&._formPanel, ._formPanel {
border-top: none;
}
}
&:not(:last-child) {
&._formPanel, ._formPanel {
border-bottom: solid 0.5px var(--divider);
}
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<div class="yzpgjkxe _formItem">
<div class="_formLabel"><slot name="label"></slot></div>
<button class="main _button _formPanel _formClickable" :class="{ center, primary, danger }">
<slot></slot>
<div class="suffix">
<slot name="suffix"></slot>
<div class="icon">
<slot name="suffixIcon"></slot>
</div>
</div>
</button>
<div class="_formCaption"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import './form.scss';
export default defineComponent({
props: {
primary: {
type: Boolean,
required: false,
default: false,
},
danger: {
type: Boolean,
required: false,
default: false,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
center: {
type: Boolean,
required: false,
default: true,
}
},
});
</script>
<style lang="scss" scoped>
.yzpgjkxe {
> .main {
display: flex;
width: 100%;
box-sizing: border-box;
padding: 14px 16px;
text-align: left;
align-items: center;
&.center {
display: block;
text-align: center;
}
&.primary {
color: var(--accent);
}
&.danger {
color: #ff2a2a;
}
> .suffix {
display: inline-flex;
margin-left: auto;
opacity: 0.7;
> .icon {
margin-left: 1em;
}
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
._formPanel {
background: var(--panel);
border-radius: var(--radius);
&._formClickable {
&:hover {
background: var(--panelHighlight);
}
}
}
._formLabel {
font-size: 80%;
padding: 0 16px 8px 16px;
&:empty {
display: none;
}
}
._formCaption {
font-size: 80%;
padding: 8px 16px 0 16px;
&:empty {
display: none;
}
}
._formItem {
& + ._formItem {
margin-top: 24px;
}
}

View File

@@ -0,0 +1,42 @@
<template>
<div class="vrtktovg _formItem" v-size="{ max: [500] }">
<div class="_formLabel"><slot name="label"></slot></div>
<div class="main _form_group">
<slot></slot>
</div>
<div class="_formCaption"><slot name="caption"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
});
</script>
<style lang="scss" scoped>
.vrtktovg {
> .main {
> ::v-deep(*) {
margin: 0;
&:not(:first-child) {
&._formPanel, ._formPanel {
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
}
&:not(:last-child) {
&._formPanel, ._formPanel {
border-bottom: solid 0.5px var(--divider);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,306 @@
<template>
<div class="ztzhwixg _formItem" :class="{ inline, disabled }">
<div class="_formLabel"><slot></slot></div>
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input _formPanel">
<div class="prefix" ref="prefixEl"><slot name="prefix"></slot></div>
<input v-if="debounce" ref="inputEl"
v-debounce="500"
:type="type"
v-model.lazy="v"
:disabled="disabled"
:required="required"
:readonly="readonly"
:placeholder="placeholder"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
:step="step"
@focus="focused = true"
@blur="focused = false"
@keydown="onKeydown($event)"
@input="onInput"
:list="id"
>
<input v-else ref="inputEl"
:type="type"
v-model="v"
:disabled="disabled"
:required="required"
:readonly="readonly"
:placeholder="placeholder"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
:step="step"
@focus="focused = true"
@blur="focused = false"
@keydown="onKeydown($event)"
@input="onInput"
:list="id"
>
<datalist :id="id" v-if="datalist">
<option v-for="data in datalist" :value="data"/>
</datalist>
<div class="suffix" ref="suffixEl"><slot name="suffix"></slot></div>
</div>
<button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
<div class="_formCaption"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs } from 'vue';
import debounce from 'v-debounce';
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
import './form.scss';
export default defineComponent({
directives: {
debounce
},
props: {
value: {
required: false
},
type: {
type: String,
required: false
},
required: {
type: Boolean,
required: false
},
readonly: {
type: Boolean,
required: false
},
disabled: {
type: Boolean,
required: false
},
pattern: {
type: String,
required: false
},
placeholder: {
type: String,
required: false
},
autofocus: {
type: Boolean,
required: false,
default: false
},
autocomplete: {
required: false
},
spellcheck: {
required: false
},
step: {
required: false
},
debounce: {
required: false
},
datalist: {
type: Array,
required: false,
},
inline: {
type: Boolean,
required: false,
default: false
},
save: {
type: Function,
required: false,
},
},
emits: ['change', 'keydown', 'enter'],
setup(props, context) {
const { value, type, autofocus } = toRefs(props);
const v = ref(value.value);
const id = Math.random().toString(); // TODO: uuid?
const focused = ref(false);
const changed = ref(false);
const invalid = ref(false);
const filled = computed(() => v.value !== '' && v.value != null);
const inputEl = ref(null);
const prefixEl = ref(null);
const suffixEl = ref(null);
const focus = () => inputEl.value.focus();
const onInput = (ev) => {
changed.value = true;
context.emit('change', ev);
};
const onKeydown = (ev: KeyboardEvent) => {
context.emit('keydown', ev);
if (ev.code === 'Enter') {
context.emit('enter');
}
};
watch(value, newValue => {
v.value = newValue;
});
watch(v, newValue => {
if (type?.value === 'number') {
context.emit('update:value', parseFloat(newValue));
} else {
context.emit('update:value', newValue);
}
invalid.value = inputEl.value.validity.badInput;
});
onMounted(() => {
nextTick(() => {
if (autofocus.value) {
focus();
}
// このコンポーネントが作成された時、非表示状態である場合がある
// 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
const clock = setInterval(() => {
if (prefixEl.value) {
if (prefixEl.value.offsetWidth) {
inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
}
}
if (suffixEl.value) {
if (suffixEl.value.offsetWidth) {
inputEl.value.style.paddingRight = suffixEl.value.offsetWidth + 'px';
}
}
}, 100);
onUnmounted(() => {
clearInterval(clock);
});
});
});
return {
id,
v,
focused,
invalid,
changed,
filled,
inputEl,
prefixEl,
suffixEl,
focus,
onInput,
onKeydown,
faExclamationCircle,
};
},
});
</script>
<style lang="scss" scoped>
.ztzhwixg {
position: relative;
> .icon {
position: absolute;
top: 0;
left: 0;
width: 24px;
text-align: center;
line-height: 32px;
&:not(:empty) + .input {
margin-left: 28px;
}
}
> .input {
$height: 52px;
position: relative;
> input {
display: block;
height: $height;
width: 100%;
margin: 0;
padding: 0 16px;
font: inherit;
font-weight: normal;
font-size: 1em;
line-height: $height;
color: var(--inputText);
background: transparent;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
box-sizing: border-box;
&[type='file'] {
display: none;
}
}
> .prefix,
> .suffix {
display: block;
position: absolute;
z-index: 1;
top: 0;
padding: 0 16px;
font-size: 1em;
line-height: $height;
color: var(--inputLabel);
pointer-events: none;
&:empty {
display: none;
}
> * {
display: inline-block;
min-width: 16px;
max-width: 150px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
> .prefix {
left: 0;
padding-right: 8px;
}
> .suffix {
right: 0;
padding-left: 8px;
}
}
> .save {
margin: 6px 0 0 0;
font-size: 0.8em;
}
&.inline {
display: inline-block;
margin: 0;
}
&.disabled {
opacity: 0.7;
&, * {
cursor: not-allowed !important;
}
}
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<div class="_formItem">
<div class="_formPanel anocepby">
<span class="key"><slot name="key"></slot></span>
<span class="value"><slot name="value"></slot></span>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import './form.scss';
export default defineComponent({
});
</script>
<style lang="scss" scoped>
.anocepby {
display: flex;
align-items: center;
padding: 14px 16px;
> .value {
margin-left: auto;
opacity: 0.7;
}
}
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div class="qmfkfnzi _formItem">
<a class="main _button _formPanel _formClickable" :href="to" target="_blank" v-if="external">
<span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot></slot></span>
<Fa :icon="faExternalLinkAlt" class="right"/>
</a>
<MkA class="main _button _formPanel _formClickable" :class="{ active }" :to="to" v-else>
<span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot></slot></span>
<Fa :icon="faChevronRight" class="right"/>
</MkA>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faChevronRight, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
import './form.scss';
export default defineComponent({
props: {
to: {
type: String,
required: true
},
active: {
type: Boolean,
required: false
},
external: {
type: Boolean,
required: false
},
},
data() {
return {
faChevronRight, faExternalLinkAlt
};
}
});
</script>
<style lang="scss" scoped>
.qmfkfnzi {
> .main {
display: flex;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 14px 16px 14px 14px;
&:hover {
text-decoration: none;
}
&.active {
color: var(--accent);
}
> .icon {
width: 32px;
margin-right: 2px;
flex-shrink: 0;
text-align: center;
opacity: 0.8;
&:empty {
display: none;
& + .text {
padding-left: 4px;
}
}
}
> .text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
padding-right: 12px;
}
> .right {
margin-left: auto;
opacity: 0.7;
}
}
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<FormGroup class="uljviswt _formItem">
<template #label><slot name="label"></slot></template>
<slot :items="items"></slot>
<div class="empty" v-if="empty" key="_empty_">
<slot name="empty"></slot>
</div>
<FormButton v-show="more" class="button" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary>
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</FormButton>
</FormGroup>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import FormButton from './button.vue';
import FormGroup from './group.vue';
import paging from '@/scripts/paging';
export default defineComponent({
components: {
FormButton,
FormGroup,
},
mixins: [
paging({}),
],
props: {
pagination: {
required: true
},
},
});
</script>
<style lang="scss" scoped>
.uljviswt {
}
</style>

View File

@@ -0,0 +1,106 @@
<script lang="ts">
import { defineComponent, h } from 'vue';
import MkRadio from '@/components/ui/radio.vue';
import './form.scss';
export default defineComponent({
components: {
MkRadio
},
props: {
modelValue: {
required: false
},
},
data() {
return {
value: this.modelValue,
}
},
watch: {
value() {
this.$emit('update:modelValue', this.value);
}
},
render() {
const label = this.$slots.desc();
const options = this.$slots.default();
return h('div', {
class: 'cnklmpwm _formItem'
}, [
h('div', {
class: '_formLabel',
}, label),
...options.map(option => h('button', {
class: '_button _formPanel _formClickable',
key: option.props.value,
onClick: () => this.value = option.props.value,
}, [h('span', {
class: ['check', { checked: this.value === option.props.value }],
}), option.children]))
]);
}
});
</script>
<style lang="scss">
.cnklmpwm {
> button {
display: block;
width: 100%;
box-sizing: border-box;
padding: 14px 18px;
text-align: left;
&:not(:first-of-type) {
border-top: none !important;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
&:not(:last-of-type) {
border-bottom: solid 0.5px var(--divider);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
> .check {
display: inline-block;
vertical-align: bottom;
position: relative;
width: 20px;
height: 20px;
margin-right: 8px;
background: none;
border: 2px solid var(--inputBorder);
border-radius: 100%;
transition: inherit;
&:after {
content: "";
display: block;
position: absolute;
top: 3px;
right: 3px;
bottom: 3px;
left: 3px;
border-radius: 100%;
opacity: 0;
transform: scale(0);
transition: .4s cubic-bezier(.25,.8,.25,1);
}
&.checked {
border-color: var(--accent);
&:after {
background-color: var(--accent);
transform: scale(1);
opacity: 1;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,122 @@
<template>
<div class="ifitouly _formItem" :class="{ focused, disabled }">
<div class="_formLabel"><slot name="label"></slot></div>
<div class="_formPanel main">
<input
type="range"
ref="input"
v-model="v"
:disabled="disabled"
:min="min"
:max="max"
:step="step"
@focus="focused = true"
@blur="focused = false"
@input="$emit('update:value', $event.target.value)"
/>
</div>
<div class="_formCaption"><slot name="caption"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
value: {
type: Number,
required: false,
default: 0
},
disabled: {
type: Boolean,
required: false,
default: false
},
min: {
type: Number,
required: false,
default: 0
},
max: {
type: Number,
required: false,
default: 100
},
step: {
type: Number,
required: false,
default: 1
},
},
data() {
return {
v: this.value,
focused: false
};
},
watch: {
value(v) {
this.v = parseFloat(v);
}
},
});
</script>
<style lang="scss" scoped>
.ifitouly {
position: relative;
> .main {
padding: 24px 16px;
> input {
display: block;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: var(--X10);
height: 4px;
width: 100%;
box-sizing: border-box;
margin: 0;
outline: 0;
border: 0;
border-radius: 7px;
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
box-sizing: content-box;
}
&::-moz-range-thumb {
-moz-appearance: none;
appearance: none;
cursor: pointer;
width: 20px;
height: 20px;
display: block;
border-radius: 50%;
border: none;
background: var(--accent);
box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
}
}
}
}
</style>

View File

@@ -0,0 +1,147 @@
<template>
<div class="yrtfrpux _formItem" :class="{ disabled, inline }">
<div class="_formLabel"><slot name="label"></slot></div>
<div class="icon" ref="icon"><slot name="icon"></slot></div>
<div class="input _formPanel _formClickable" @click="focus">
<div class="prefix" ref="prefix"><slot name="prefix"></slot></div>
<select ref="input"
v-model="v"
:required="required"
:disabled="disabled"
@focus="focused = true"
@blur="focused = false"
>
<slot></slot>
</select>
<div class="suffix">
<Fa :icon="faChevronDown"/>
</div>
</div>
<div class="_formCaption"><slot name="caption"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
import './form.scss';
export default defineComponent({
props: {
value: {
required: false
},
required: {
type: Boolean,
required: false
},
disabled: {
type: Boolean,
required: false
},
inline: {
type: Boolean,
required: false,
default: false
},
},
data() {
return {
faChevronDown,
};
},
computed: {
v: {
get() {
return this.value;
},
set(v) {
this.$emit('update:value', v);
}
},
},
methods: {
focus() {
this.$refs.input.focus();
}
}
});
</script>
<style lang="scss" scoped>
.yrtfrpux {
position: relative;
> .icon {
position: absolute;
top: 0;
left: 0;
width: 24px;
text-align: center;
line-height: 32px;
&:not(:empty) + .input {
margin-left: 28px;
}
}
> .input {
display: flex;
position: relative;
> select {
display: block;
flex: 1;
width: 100%;
padding: 0 16px;
font: inherit;
font-weight: normal;
font-size: 1em;
height: 52px;
background: none;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
appearance: none;
-webkit-appearance: none;
color: var(--fg);
option,
optgroup {
color: var(--fg);
background: var(--bg);
}
}
> .prefix,
> .suffix {
display: block;
align-self: center;
justify-self: center;
font-size: 1em;
line-height: 32px;
color: var(--inputLabel);
pointer-events: none;
&:empty {
display: none;
}
> * {
display: block;
min-width: 16px;
}
}
> .prefix {
padding-right: 4px;
}
> .suffix {
padding: 0 16px 0 0;
opacity: 0.7;
}
}
}
</style>

View File

@@ -0,0 +1,132 @@
<template>
<div class="ijnpvmgr _formItem">
<div class="main _formPanel _formClickable"
:class="{ disabled, checked }"
:aria-checked="checked"
:aria-disabled="disabled"
@click.prevent="toggle"
>
<input
type="checkbox"
ref="input"
:disabled="disabled"
@keydown.enter="toggle"
>
<span class="button">
<span></span>
</span>
<span class="label">
<span><slot></slot></span>
</span>
</div>
<div class="_formCaption"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import './form.scss';
export default defineComponent({
props: {
value: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
},
computed: {
checked(): boolean {
return this.value;
}
},
methods: {
toggle() {
if (this.disabled) return;
this.$emit('update:value', !this.checked);
}
}
});
</script>
<style lang="scss" scoped>
.ijnpvmgr {
> .main {
position: relative;
display: flex;
padding: 16px;
cursor: pointer;
> * {
user-select: none;
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.checked {
> .button {
background-color: var(--X10);
border-color: var(--X10);
> * {
background-color: var(--accent);
transform: translateX(14px);
}
}
}
> input {
position: absolute;
width: 0;
height: 0;
opacity: 0;
margin: 0;
}
> .button {
position: relative;
display: inline-block;
flex-shrink: 0;
margin: 3px 0 0 0;
width: 34px;
height: 14px;
background: var(--X6);
outline: none;
border-radius: 14px;
transition: all 0.3s;
cursor: pointer;
> * {
position: absolute;
top: -3px;
left: 0;
border-radius: 100%;
transition: background-color 0.3s, transform 0.3s;
width: 20px;
height: 20px;
background-color: #fff;
box-shadow: 0 2px 1px -1px rgba(#000, 0.2), 0 1px 1px 0 rgba(#000, 0.14), 0 1px 3px 0 rgba(#000, 0.12);
}
}
> .label {
margin-left: 12px;
display: block;
transition: inherit;
color: var(--fg);
> span {
display: block;
line-height: 20px;
transition: inherit;
}
}
}
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<div class="rivhosbp _formItem" :class="{ tall, pre }">
<div class="_formLabel"><slot></slot></div>
<div class="input _formPanel">
<textarea ref="input" :class="{ code, _monospace: code }"
:value="value"
:required="required"
:readonly="readonly"
:pattern="pattern"
:autocomplete="autocomplete"
:spellcheck="!code"
@input="onInput"
@focus="focused = true"
@blur="focused = false"
></textarea>
</div>
<button class="save _textButton" v-if="save && changed" @click="() => { changed = false; save(); }">{{ $t('save') }}</button>
<div class="_formCaption"><slot name="desc"></slot></div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import './form.scss';
export default defineComponent({
props: {
value: {
required: false
},
required: {
type: Boolean,
required: false
},
readonly: {
type: Boolean,
required: false
},
pattern: {
type: String,
required: false
},
autocomplete: {
type: String,
required: false
},
code: {
type: Boolean,
required: false
},
tall: {
type: Boolean,
required: false,
default: false
},
pre: {
type: Boolean,
required: false,
default: false
},
save: {
type: Function,
required: false,
},
},
data() {
return {
changed: false,
}
},
methods: {
focus() {
this.$refs.input.focus();
},
onInput(ev) {
this.changed = true;
this.$emit('update:value', ev.target.value);
}
}
});
</script>
<style lang="scss" scoped>
.rivhosbp {
position: relative;
> .input {
position: relative;
> textarea {
display: block;
width: 100%;
min-width: 100%;
max-width: 100%;
min-height: 130px;
margin: 0;
padding: 16px;
box-sizing: border-box;
font: inherit;
font-weight: normal;
font-size: 1em;
background: transparent;
border: none;
border-radius: 0;
outline: none;
box-shadow: none;
color: var(--fg);
&.code {
tab-size: 2;
}
}
}
> .save {
margin: 6px 0 0 0;
font-size: 0.8em;
}
&.tall {
> .input {
> textarea {
min-height: 200px;
}
}
}
&.pre {
> .input {
> textarea {
white-space: pre;
}
}
}
}
</style>

View File

@@ -0,0 +1,36 @@
<template>
<div class="wthhikgt _formItem" v-size="{ max: [500] }">
<slot></slot>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
});
</script>
<style lang="scss" scoped>
.wthhikgt {
position: relative;
display: flex;
> ::v-deep(*) {
flex: 1;
margin: 0;
&:not(:last-child) {
margin-right: 16px;
}
}
&.max-width_500px {
display: block;
> ::v-deep(*) {
margin: inherit;
}
}
}
</style>

View File

@@ -0,0 +1,152 @@
<template>
<MkModal ref="modal" @click="$refs.modal.close()" @closed="$emit('closed')">
<div class="szkkfdyq _popup">
<div class="main">
<template v-for="item in items">
<button v-if="item.action" class="_button" @click="$event => { item.action($event); close(); }">
<Fa :icon="item.icon" class="icon"/>
<div class="text">{{ item.text }}</div>
<i v-if="item.indicate"><Fa :icon="faCircle"/></i>
</button>
<MkA v-else :to="item.to" @click.passive="close()">
<Fa :icon="item.icon" class="icon"/>
<div class="text">{{ item.text }}</div>
<i v-if="item.indicate"><Fa :icon="faCircle"/></i>
</MkA>
</template>
</div>
<div class="sub">
<MkA to="/docs" @click.passive="close()">
<Fa :icon="faQuestionCircle" class="icon"/>
<div class="text">{{ $t('help') }}</div>
</MkA>
<MkA to="/about" @click.passive="close()">
<Fa :icon="faInfoCircle" class="icon"/>
<div class="text">{{ $t('aboutX', { x: instanceName }) }}</div>
</MkA>
<MkA to="/about-misskey" @click.passive="close()">
<Fa :icon="faInfoCircle" class="icon"/>
<div class="text">{{ $t('aboutMisskey') }}</div>
</MkA>
</div>
</div>
</MkModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faQuestionCircle, faInfoCircle, faCircle } from '@fortawesome/free-solid-svg-icons';
import MkModal from '@/components/ui/modal.vue';
import { sidebarDef } from '@/sidebar';
import { instanceName } from '@/config';
export default defineComponent({
components: {
MkModal,
},
emits: ['closed'],
data() {
return {
menuDef: sidebarDef,
items: [],
instanceName,
faQuestionCircle, faInfoCircle, faCircle,
};
},
computed: {
menu(): string[] {
return this.$store.state.deviceUser.menu;
},
},
created() {
this.items = Object.keys(this.menuDef).filter(k => !this.menu.includes(k)).map(k => this.menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
type: def.to ? 'link' : 'button',
text: this.$t(def.title),
icon: def.icon,
to: def.to,
action: def.action,
indicate: def.indicated,
}));
},
methods: {
close() {
this.$refs.modal.close();
}
}
});
</script>
<style lang="scss" scoped>
.szkkfdyq {
width: 100%;
max-height: 100%;
max-width: 800px;
padding: 32px;
box-sizing: border-box;
overflow: auto;
text-align: center;
border-radius: 16px;
@media (max-width: 500px) {
padding: 16px;
}
> .main, > .sub {
> * {
position: relative;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 128px;
height: 128px;
border-radius: var(--radius);
@media (max-width: 500px) {
width: 100px;
height: 100px;
}
&:hover {
background: rgba(0, 0, 0, 0.05);
text-decoration: none;
}
> .icon {
font-size: 26px;
}
> .text {
margin-top: 8px;
font-size: 0.9em;
line-height: 1.5em;
}
> i {
position: absolute;
top: 32px;
left: 32px;
color: var(--indicator);
font-size: 8px;
animation: blink 1s infinite;
@media (max-width: 500px) {
top: 16px;
left: 16px;
}
}
}
}
> .sub {
margin-top: 8px;
padding-top: 8px;
border-top: solid 1px var(--divider);
}
}
</style>

View File

@@ -68,7 +68,7 @@ export default defineComponent({
created() {
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
this.$watch('image', () => {
this.hide = this.image.isSensitive && !this.$store.state.device.alwaysShowNsfw;
this.hide = (this.$store.state.device.nsfw === 'force') ? true : this.image.isSensitive && (this.$store.state.device.nsfw !== 'ignore');
if (this.image.blurhash) {
this.color = extractAvgColorFromBlurhash(this.image.blurhash);
}

View File

@@ -48,7 +48,7 @@ export default defineComponent({
}
},
created() {
this.hide = this.video.isSensitive && !this.$store.state.device.alwaysShowNsfw;
this.hide = (this.$store.state.device.nsfw === 'force') ? true : this.video.isSensitive && (this.$store.state.device.nsfw !== 'ignore');
},
});
</script>

View File

@@ -500,7 +500,7 @@ export default defineComponent({
this.blur();
os.popup(import('@/components/emoji-picker.vue'), {
src: this.$refs.reactButton,
compact: !this.$store.state.device.useFullReactionPicker
asReactionPicker: true
}, {
done: reaction => {
if (reaction) {

View File

@@ -8,7 +8,7 @@
<MkError v-if="error" @retry="init()"/>
<div v-show="more && reversed" style="margin-bottom: var(--margin);">
<button class="_loadMore" v-appear="$store.state.device.enableInfiniteScroll ? fetchMore : null" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<button class="_loadMore" @click="fetchMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
<template v-if="!moreFetching">{{ $t('loadMore') }}</template>
<template v-if="moreFetching"><MkLoading inline/></template>
</button>

View File

@@ -45,7 +45,7 @@
import { defineComponent } from 'vue';
import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faServer, faInfoCircle, faQuestionCircle, faProjectDiagram, faStream, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons';
import { host, instanceName } from '@/config';
import { host } from '@/config';
import { search } from '@/scripts/search';
import * as os from '@/os';
import { sidebarDef } from '@/sidebar';
@@ -223,30 +223,8 @@ export default defineComponent({
},
more(ev) {
const items = Object.keys(this.menuDef).filter(k => !this.menu.includes(k)).map(k => this.menuDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
type: def.to ? 'link' : 'button',
text: this.$t(def.title),
icon: def.icon,
to: def.to,
action: def.action,
indicate: def.indicated,
}));
os.modalMenu([...items, null, {
type: 'link',
text: this.$t('help'),
to: '/docs',
icon: faQuestionCircle,
}, {
type: 'link',
text: this.$t('aboutX', { x: instanceName }),
to: '/about',
icon: faInfoCircle,
}, {
type: 'link',
text: this.$t('aboutMisskey'),
to: '/about-misskey',
icon: faInfoCircle,
}], ev.currentTarget || ev.target);
os.popup(import('./launch-pad.vue'), {}, {
}, 'closed');
},
addAcount() {

View File

@@ -1,26 +1,32 @@
<template>
<div class="pxhvhrfw" v-size="{ max: [500] }">
<button v-for="item in items" class="_button" @click="$emit('update:value', item.value)" :class="{ active: value === item.value }" :disabled="value === item.value" :key="item.value"><Fa v-if="item.icon" :icon="item.icon" class="icon"/>{{ item.label }}</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, h, resolveDirective, withDirectives } from 'vue';
export default defineComponent({
props: {
items: {
type: Array,
required: true,
},
value: {
required: true,
},
},
render() {
const options = this.$slots.default();
return withDirectives(h('div', {
class: 'pxhvhrfw',
}, options.map(option => h('button', {
class: ['_button', { active: this.value === option.props.value }],
key: option.props.value,
disabled: this.value === option.props.value,
onClick: () => {
this.$emit('update:value', option.props.value);
}
}, option.children))), [
[resolveDirective('size'), { max: [500] }]
]);
}
});
</script>
<style lang="scss" scoped>
<style lang="scss">
.pxhvhrfw {
display: flex;

View File

@@ -9,10 +9,13 @@
<template #header>Req Viewer</template>
<div class="rlkneywz">
<MkTab v-model:value="tab" :items="[{ label: 'Request', value: 'req', }, { label: 'Response', value: 'res', }]" style="border-bottom: solid 1px var(--divider);"/>
<MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);">
<option value="req">Request</option>
<option value="res">Response</option>
</MkTab>
<code v-if="tab === 'req'">{{ reqStr }}</code>
<code v-if="tab === 'res'">{{ resStr }}</code>
<code v-if="tab === 'req'" class="_monospace">{{ reqStr }}</code>
<code v-if="tab === 'res'" class="_monospace">{{ resStr }}</code>
</div>
</XWindow>
</template>
@@ -64,7 +67,6 @@ export default defineComponent({
font-size: 0.9em;
tab-size: 2;
white-space: pre;
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
}
</style>

View File

@@ -3,8 +3,13 @@
<template #header>
<Fa :icon="faTerminal" style="margin-right: 0.5em;"/>Task Manager
</template>
<div class="qljqmnzj">
<MkTab v-model:value="tab" :items="[{ label: 'Windows', value: 'windows', }, { label: 'Stream', value: 'stream', }, { label: 'Stream (Pool)', value: 'streamPool', }, { label: 'API', value: 'api', }]" style="border-bottom: solid 1px var(--divider);"/>
<div class="qljqmnzj _monospace">
<MkTab v-model:value="tab" style="border-bottom: solid 1px var(--divider);">
<option value="windows">Windows</option>
<option value="stream">Stream</option>
<option value="streamPool">Stream (Pool)</option>
<option value="api">API</option>
</MkTab>
<div class="content">
<div v-if="tab === 'windows'" class="windows" v-follow>
@@ -145,7 +150,6 @@ export default defineComponent({
display: flex;
flex-direction: column;
height: 100%;
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
> .content {
flex: 1;

View File

@@ -6,6 +6,7 @@
import { defineComponent } from 'vue';
import XNotes from './notes.vue';
import * as os from '@/os';
import * as sound from '@/scripts/sound';
export default defineComponent({
components: {
@@ -65,7 +66,7 @@ export default defineComponent({
this.$emit('note');
if (this.sound) {
os.sound(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
sound.play(note.userId === this.$store.state.i.id ? 'noteMy' : 'note');
}
};

View File

@@ -7,9 +7,6 @@ export default defineComponent({
MkRadio
},
props: {
defs: {
required: true
},
modelValue: {
required: false
},
@@ -43,7 +40,7 @@ export default defineComponent({
});
</script>
<style lang="scss" scoped>
<style lang="scss">
.novjtcto {
margin: 32px 0;

View File

@@ -1,7 +1,7 @@
<template>
<div class="timctyfi" :class="{ focused, disabled }">
<div class="icon"><slot name="icon"></slot></div>
<span class="title"><slot name="title"></slot></span>
<span class="label"><slot name="label"></slot></span>
<input
type="range"
ref="input"
@@ -19,7 +19,7 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';import * as os from '@/os';
import { defineComponent } from 'vue';
export default defineComponent({
props: {

View File

@@ -17,10 +17,8 @@
<span></span>
</span>
<span class="label">
<span :aria-hidden="!checked"><slot></slot></span>
<p :aria-hidden="!checked">
<slot name="desc"></slot>
</p>
<span><slot></slot></span>
<p><slot name="desc"></slot></p>
</span>
</div>
</template>

View File

@@ -2,7 +2,7 @@
<div class="adhpbeos" :class="{ focused, filled, tall, pre }">
<div class="input">
<span class="label" ref="label"><slot></slot></span>
<textarea ref="input" :class="{ code }"
<textarea ref="input" :class="{ code, _monospace: code }"
:value="value"
:required="required"
:readonly="readonly"
@@ -166,7 +166,6 @@ export default defineComponent({
&.code {
tab-size: 2;
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
}
}

View File

@@ -16,7 +16,8 @@ import { router } from './router';
import { applyTheme } from '@/scripts/theme';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import { i18n, lang } from './i18n';
import { stream, sound, isMobile, dialog } from '@/os';
import { stream, isMobile, dialog } from '@/os';
import * as sound from './scripts/sound';
console.info(`Misskey v${version}`);
@@ -50,7 +51,7 @@ if (_DEV_) {
document.addEventListener('touchend', () => {}, { passive: true });
if (localStorage.getItem('theme') == null) {
applyTheme(require('@/themes/l-white.json5'));
applyTheme(require('@/themes/l-light.json5'));
}
//#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
@@ -307,7 +308,7 @@ if (store.getters.isSignedIn) {
hasUnreadMessagingMessage: true
});
sound('chatBg');
sound.play('chatBg');
});
main.on('readAllAntennas', () => {
@@ -321,7 +322,7 @@ if (store.getters.isSignedIn) {
hasUnreadAntenna: true
});
sound('antenna');
sound.play('antenna');
});
main.on('readAllAnnouncements', () => {
@@ -341,7 +342,7 @@ if (store.getters.isSignedIn) {
hasUnreadChannel: true
});
sound('channel');
sound.play('channel');
});
main.on('readAllAnnouncements', () => {

View File

@@ -6,6 +6,7 @@ import { apiUrl, debug } from '@/config';
import MkPostFormDialog from '@/components/post-form-dialog.vue';
import MkWaitingDialog from '@/components/waiting-dialog.vue';
import { resolve } from '@/router';
import { device } from './cold-storage';
const ua = navigator.userAgent.toLowerCase();
export const isMobile = /mobile|iphone|ipad|android/.test(ua);
@@ -344,15 +345,6 @@ export function post(props: Record<string, any>) {
});
}
export function sound(type: string) {
if (store.state.device.sfxVolume === 0) return;
const sound = store.state.device['sfx' + type.substr(0, 1).toUpperCase() + type.substr(1)];
if (sound == null) return;
const audio = new Audio(`/assets/sounds/${sound}.mp3`);
audio.volume = store.state.device.sfxVolume;
audio.play();
}
export const deckGlobalEvents = new EventEmitter();
export const uploads = ref([]);

View File

@@ -1,6 +1,6 @@
<template>
<div class="_section">
<MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content" ref="list">
<MkPagination :pagination="pagination" #default="{items}" class="ruryvtyk _content">
<section class="_card announcement _vMargin" v-for="(announcement, i) in items" :key="announcement.id">
<div class="_title"><span v-if="$store.getters.isSignedIn && !announcement.isRead">🆕 </span>{{ announcement.title }}</div>
<div class="_content">

View File

@@ -20,7 +20,7 @@
</div>
</div>
<XPostForm :channel="channel" class="post-form _content _panel _vMargin" fixed/>
<XPostForm :channel="channel" class="post-form _content _panel _vMargin" fixed v-if="this.$store.getters.isSignedIn"/>
<XTimeline class="_content _vMargin" src="channel" :channel="channelId" @before="before" @after="after"/>
</div>

View File

@@ -1,26 +1,30 @@
<template>
<div>
<div class="_section" style="padding: 0;">
<MkTab class="_content" v-model:value="tab" :items="[{ label: $t('_channel.featured'), value: 'featured', icon: faFireAlt }, { label: $t('_channel.following'), value: 'following', icon: faHeart }, { label: $t('_channel.owned'), value: 'owned', icon: faEdit }]"/>
<div class="_section" style="padding: 0;" v-if="this.$store.getters.isSignedIn">
<MkTab class="_content" v-model:value="tab">
<option value="featured"><Fa :icon="faFireAlt"/> {{ $t('_channel.featured') }}</option>
<option value="following"><Fa :icon="faHeart"/> {{ $t('_channel.following') }}</option>
<option value="owned"><Fa :icon="faEdit"/> {{ $t('_channel.owned') }}</option>
</MkTab>
</div>
<div class="_section">
<div class="_content grwlizim featured" v-if="tab === 'featured'">
<MkPagination :pagination="featuredPagination" #default="{items}">
<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
<MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/>
</MkPagination>
</div>
<div class="_content grwlizim following" v-if="tab === 'following'">
<MkPagination :pagination="followingPagination" #default="{items}">
<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
<MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/>
</MkPagination>
</div>
<div class="_content grwlizim owned" v-if="tab === 'owned'">
<MkButton class="new" @click="create()"><Fa :icon="faPlus"/></MkButton>
<MkPagination :pagination="ownedPagination" #default="{items}">
<MkChannelPreview v-for="channel in items" class="uveselbe" :channel="channel" :key="channel.id"/>
<MkChannelPreview v-for="channel in items" class="_vMargin" :channel="channel" :key="channel.id"/>
</MkPagination>
</div>
</div>
@@ -44,7 +48,11 @@ export default defineComponent({
return {
INFO: {
title: this.$t('channel'),
icon: faSatelliteDish
icon: faSatelliteDish,
action: {
icon: faPlus,
handler: this.create
}
},
tab: 'featured',
featuredPagination: {
@@ -69,23 +77,3 @@ export default defineComponent({
}
});
</script>
<style lang="scss" scoped>
.grwlizim {
padding: 16px 0;
&.my .uveselbe:first-child {
margin-top: 16px;
}
.uveselbe:not(:last-child) {
margin-bottom: 8px;
}
@media (min-width: 500px) {
.uveselbe:not(:last-child) {
margin-bottom: 16px;
}
}
}
</style>

View File

@@ -1,7 +1,10 @@
<template>
<div class="mk-instance-emojis">
<div class="_section" style="padding: 0;">
<MkTab v-model:value="tab" :items="[{ label: $t('local'), value: 'local' }, { label: $t('remote'), value: 'remote' }]"/>
<MkTab v-model:value="tab">
<option value="local">{{ $t('local') }}</option>
<option value="remote">{{ $t('remote') }}</option>
</MkTab>
</div>
<div class="_section">

View File

@@ -1,12 +1,14 @@
<template>
<div v-if="meta">
<section class="_section info">
<div v-if="meta" class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faInfoCircle"/> {{ $t('basicInfo') }}</div>
<div class="_content">
<MkInput v-model:value="name">{{ $t('instanceName') }}</MkInput>
<MkTextarea v-model:value="description">{{ $t('instanceDescription') }}</MkTextarea>
<MkInput v-model:value="iconUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('iconUrl') }}</MkInput>
<MkInput v-model:value="bannerUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('bannerUrl') }}</MkInput>
<MkInput v-model:value="backgroundImageUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('backgroundImageUrl') }}</MkInput>
<MkInput v-model:value="logoImageUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('logoImageUrl') }}</MkInput>
<MkInput v-model:value="tosUrl"><template #icon><Fa :icon="faLink"/></template>{{ $t('tosUrl') }}</MkInput>
<MkInput v-model:value="maintainerName">{{ $t('maintainerName') }}</MkInput>
<MkInput v-model:value="maintainerEmail" type="email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $t('maintainerEmail') }}</MkInput>
@@ -16,7 +18,7 @@
</div>
</section>
<section class="_section info">
<section class="_card _vMargin">
<div class="_content">
<MkInput v-model:value="maxNoteTextLength" type="number" :save="() => save()"><template #icon><Fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</MkInput>
</div>
@@ -30,7 +32,7 @@
</div>
</section>
<section class="_section info">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faUser"/> {{ $t('registration') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableRegistration" @update:value="save()">{{ $t('enableRegistration') }}</MkSwitch>
@@ -38,7 +40,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faShieldAlt"/> {{ $t('hcaptcha') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableHcaptcha">{{ $t('enableHcaptcha') }}</MkSwitch>
@@ -56,7 +58,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faShieldAlt"/> {{ $t('recaptcha') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableRecaptcha" ref="enableRecaptcha">{{ $t('enableRecaptcha') }}</MkSwitch>
@@ -74,7 +76,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faEnvelope" /> {{ $t('emailConfig') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableEmail" @update:value="save()">{{ $t('enableEmail') }}<template #desc>{{ $t('emailConfigInfo') }}</template></MkSwitch>
@@ -97,7 +99,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faBolt"/> {{ $t('serviceworker') }}</div>
<div class="_content">
<MkSwitch v-model:value="enableServiceWorker">{{ $t('enableServiceworker') }}<template #desc>{{ $t('serviceworkerInfo') }}</template></MkSwitch>
@@ -113,7 +115,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faThumbtack"/> {{ $t('pinnedUsers') }}</div>
<div class="_content">
<MkTextarea v-model:value="pinnedUsers">
@@ -125,7 +127,19 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faThumbtack"/> {{ $t('pinnedPages') }}</div>
<div class="_content">
<MkTextarea v-model:value="pinnedPages">
<template #desc>{{ $t('pinnedPagesDescription') }}</template>
</MkTextarea>
</div>
<div class="_footer">
<MkButton primary @click="save(true)"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</section>
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faCloud"/> {{ $t('files') }}</div>
<div class="_content">
<MkSwitch v-model:value="cacheRemoteFiles">{{ $t('cacheRemoteFiles') }}<template #desc>{{ $t('cacheRemoteFilesDescription') }}</template></MkSwitch>
@@ -138,7 +152,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faCloud"/> {{ $t('objectStorage') }}</div>
<div class="_content">
<MkSwitch v-model:value="useObjectStorage">{{ $t('useObjectStorage') }}</MkSwitch>
@@ -166,7 +180,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faGhost"/> {{ $t('proxyAccount') }}</div>
<div class="_content">
<MkInput :value="proxyAccount ? proxyAccount.username : null" disabled><template #prefix>@</template>{{ $t('proxyAccount') }}<template #desc>{{ $t('proxyAccountDescription') }}</template></MkInput>
@@ -174,7 +188,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faBan"/> {{ $t('blockedInstances') }}</div>
<div class="_content">
<MkTextarea v-model:value="blockedHosts">
@@ -186,7 +200,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faShareAlt"/> {{ $t('integration') }}</div>
<div class="_content">
<header><Fa :icon="faTwitter"/> Twitter</header>
@@ -220,7 +234,7 @@
</div>
</section>
<section class="_section">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faArchway" /> Summaly Proxy</div>
<div class="_content">
<MkInput v-model:value="summalyProxy">URL</MkInput>
@@ -260,6 +274,7 @@ export default defineComponent({
title: this.$t('instance'),
icon: faCog,
},
meta: null,
url,
proxyAccount: null,
proxyAccountId: null,
@@ -269,6 +284,7 @@ export default defineComponent({
remoteDriveCapacityMb: 0,
blockedHosts: '',
pinnedUsers: '',
pinnedPages: '',
maintainerName: null,
maintainerEmail: null,
name: null,
@@ -278,6 +294,8 @@ export default defineComponent({
email: null,
bannerUrl: null,
iconUrl: null,
logoImageUrl: null,
backgroundImageUrl: null,
maxNoteTextLength: 0,
enableRegistration: false,
enableLocalTimeline: false,
@@ -323,18 +341,16 @@ export default defineComponent({
}
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
async created() {
this.meta = await os.api('meta', { detail: true });
created() {
this.name = this.meta.name;
this.description = this.meta.description;
this.tosUrl = this.meta.tosUrl;
this.bannerUrl = this.meta.bannerUrl;
this.iconUrl = this.meta.iconUrl;
this.logoImageUrl = this.meta.logoImageUrl;
this.backgroundImageUrl = this.meta.backgroundImageUrl;
this.enableEmail = this.meta.enableEmail;
this.email = this.meta.email;
this.maintainerName = this.meta.maintainerName;
@@ -356,6 +372,7 @@ export default defineComponent({
this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
this.blockedHosts = this.meta.blockedHosts.join('\n');
this.pinnedUsers = this.meta.pinnedUsers.join('\n');
this.pinnedPages = this.meta.pinnedPages.join('\n');
this.enableServiceWorker = this.meta.enableServiceWorker;
this.swPublicKey = this.meta.swPublickey;
this.swPrivateKey = this.meta.swPrivateKey;
@@ -487,6 +504,8 @@ export default defineComponent({
tosUrl: this.tosUrl,
bannerUrl: this.bannerUrl,
iconUrl: this.iconUrl,
logoImageUrl: this.logoImageUrl,
backgroundImageUrl: this.backgroundImageUrl,
maintainerName: this.maintainerName,
maintainerEmail: this.maintainerEmail,
maxNoteTextLength: this.maxNoteTextLength,
@@ -506,6 +525,7 @@ export default defineComponent({
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
blockedHosts: this.blockedHosts.split('\n') || [],
pinnedUsers: this.pinnedUsers ? this.pinnedUsers.split('\n') : [],
pinnedPages: this.pinnedPages ? this.pinnedPages.split('\n') : [],
enableServiceWorker: this.enableServiceWorker,
swPublicKey: this.swPublicKey,
swPrivateKey: this.swPrivateKey,

View File

@@ -38,6 +38,7 @@ import parseAcct from '../../../misc/acct/parse';
import { isBottom, onScrollBottom, scroll } from '@/scripts/scroll';
import * as os from '@/os';
import { popout } from '@/scripts/popout';
import * as sound from '@/scripts/sound';
const Component = defineComponent({
components: {
@@ -218,7 +219,7 @@ const Component = defineComponent({
},
onMessage(message) {
os.sound('chat');
sound.play('chat');
const _isBottom = isBottom(this.$el, 64);

View File

@@ -1,7 +1,11 @@
<template>
<div class="">
<div class="_section" style="padding: 0;">
<MkTab v-model:value="tab" :items="[{ label: $t('ownedGroups'), value: 'owned' }, { label: $t('joinedGroups'), value: 'joined' }, { label: $t('invites'), icon: faEnvelopeOpenText, value: 'invites' }]"/>
<MkTab v-model:value="tab">
<option value="owned">{{ $t('ownedGroups') }}</option>
<option value="joined">{{ $t('joinedGroups') }}</option>
<option value="invites"><Fa :icon="faEnvelopeOpenText"/> {{ $t('invites') }}</option>
</MkTab>
</div>
<div class="_section">

View File

@@ -1,21 +1,31 @@
<template>
<div class="fcuexfpr">
<div v-if="note" class="note">
<div class="_section">
<XNotes v-if="showNext" class="_content" :pagination="next"/>
<MkButton v-else-if="hasNext" class="load _content" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton>
<div class="_section" v-if="showNext">
<XNotes class="_content" :pagination="next"/>
</div>
<div class="_section">
<div class="_content">
<div class="_section main">
<MkButton v-if="!showNext && hasNext" class="load next _content" @click="showNext = true"><Fa :icon="faChevronUp"/></MkButton>
<div class="_content _vMargin">
<MkRemoteCaution v-if="note.user.host != null" :href="note.url || note.uri" class="_vMargin"/>
<XNote v-model:note="note" :key="note.id" :detail="true" class="_vMargin"/>
</div>
<div class="_content clips _vMargin" v-if="clips && clips.length > 0">
<div class="title">{{ $t('clip') }}</div>
<MkA v-for="item in clips" :key="item.id" :to="`/clips/${item.id}`" class="item _panel _vMargin">
<b>{{ item.name }}</b>
<div v-if="item.description" class="description">{{ item.description }}</div>
<div class="user">
<MkAvatar :user="item.user" class="avatar"/> <MkUserName :user="item.user" :nowrap="false"/>
</div>
</MkA>
</div>
<MkButton v-if="!showPrev && hasPrev" class="load prev _content" @click="showPrev = true"><Fa :icon="faChevronDown"/></MkButton>
</div>
<div class="_section">
<XNotes v-if="showPrev" class="_content" :pagination="prev"/>
<MkButton v-else-if="hasPrev" class="load _content" @click="showPrev = true"><Fa :icon="faChevronDown"/></MkButton>
<div class="_section" v-if="showPrev">
<XNotes class="_content" :pagination="prev"/>
</div>
</div>
@@ -28,7 +38,6 @@
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { faChevronUp, faChevronDown } from '@fortawesome/free-solid-svg-icons';
import Progress from '@/scripts/loading';
import XNote from '@/components/note.vue';
import XNotes from '@/components/notes.vue';
import MkRemoteCaution from '@/components/remote-caution.vue';
@@ -55,6 +64,7 @@ export default defineComponent({
avatar: this.note.user,
} : null),
note: null,
clips: null,
hasPrev: false,
hasNext: false,
showPrev: false,
@@ -88,11 +98,13 @@ export default defineComponent({
},
methods: {
fetch() {
Progress.start();
os.api('notes/show', {
noteId: this.noteId
}).then(note => {
Promise.all([
os.api('notes/clips', {
noteId: note.id,
}),
os.api('users/notes', {
userId: note.userId,
untilId: note.id,
@@ -103,15 +115,14 @@ export default defineComponent({
sinceId: note.id,
limit: 1,
}),
]).then(([prev, next]) => {
]).then(([clips, prev, next]) => {
this.clips = clips;
this.hasPrev = prev.length !== 0;
this.hasNext = next.length !== 0;
this.note = note;
});
}).catch(e => {
this.error = e;
}).finally(() => {
Progress.done();
});
}
}
@@ -121,10 +132,46 @@ export default defineComponent({
<style lang="scss" scoped>
.fcuexfpr {
> .note {
> ._section {
> .main {
> .load {
min-width: 0;
border-radius: 999px;
&.next {
margin-bottom: var(--margin);
}
&.prev {
margin-top: var(--margin);
}
}
> .clips {
> .title {
font-weight: bold;
padding: 12px;
}
> .item {
display: block;
padding: 16px;
> .description {
padding: 8px 0;
}
> .user {
$height: 32px;
padding-top: 16px;
border-top: solid 1px var(--divider);
line-height: $height;
> .avatar {
width: $height;
height: $height;
}
}
}
}
}
}

View File

@@ -277,7 +277,7 @@ export default defineComponent({
type: 'success',
text: this.$t('_pages.created')
});
this.$router.push(`/my/pages/edit/${this.pageId}`);
this.$router.push(`/pages/edit/${this.pageId}`);
}).catch(onError);
}
},
@@ -296,7 +296,7 @@ export default defineComponent({
type: 'success',
text: this.$t('_pages.deleted')
});
this.$router.push(`/my/pages`);
this.$router.push(`/pages`);
});
});
},

View File

@@ -22,7 +22,7 @@
<div class="_content">
<MkA :to="`./${page.name}/view-source`" class="link">{{ $t('_pages.viewSource') }}</MkA>
<template v-if="$store.getters.isSignedIn && $store.state.i.id === page.userId">
<MkA :to="`/my/pages/edit/${page.id}`" class="link">{{ $t('_pages.editThisPage') }}</MkA>
<MkA :to="`/pages/edit/${page.id}`" class="link">{{ $t('_pages.editThisPage') }}</MkA>
<button v-if="$store.state.i.pinnedPageId === page.id" @click="pin(false)" class="link _textButton">{{ $t('unpin') }}</button>
<button v-else @click="pin(true)" class="link _textButton">{{ $t('pin') }}</button>
</template>

View File

@@ -1,8 +1,18 @@
<template>
<div>
<MkTab v-model:value="tab" :items="[{ label: $t('_pages.my'), value: 'my', icon: faEdit }, { label: $t('_pages.liked'), value: 'liked', icon: faHeart }]"/>
<MkTab v-model:value="tab" v-if="this.$store.getters.isSignedIn">
<option value="featured"><Fa :icon="faFireAlt"/> {{ $t('_pages.featured') }}</option>
<option value="my"><Fa :icon="faEdit"/> {{ $t('_pages.my') }}</option>
<option value="liked"><Fa :icon="faHeart"/> {{ $t('_pages.liked') }}</option>
</MkTab>
<div class="_section">
<div class="rknalgpo _content" v-if="tab === 'featured'">
<MkPagination :pagination="featuredPagesPagination" #default="{items}">
<MkPagePreview v-for="page in items" class="ckltabjg" :page="page" :key="page.id"/>
</MkPagination>
</div>
<div class="rknalgpo _content my" v-if="tab === 'my'">
<MkButton class="new" @click="create()"><Fa :icon="faPlus"/></MkButton>
<MkPagination :pagination="myPagesPagination" #default="{items}">
@@ -21,7 +31,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { faPlus, faEdit } from '@fortawesome/free-solid-svg-icons';
import { faPlus, faEdit, faFireAlt } from '@fortawesome/free-solid-svg-icons';
import { faStickyNote, faHeart } from '@fortawesome/free-regular-svg-icons';
import MkPagePreview from '@/components/page-preview.vue';
import MkPagination from '@/components/ui/pagination.vue';
@@ -42,7 +52,11 @@ export default defineComponent({
handler: this.create
}
},
tab: 'my',
tab: 'featured',
featuredPagesPagination: {
endpoint: 'pages/featured',
noPaging: true,
},
myPagesPagination: {
endpoint: 'i/pages',
limit: 5,
@@ -51,12 +65,12 @@ export default defineComponent({
endpoint: 'i/page-likes',
limit: 5,
},
faStickyNote, faPlus, faEdit, faHeart
faStickyNote, faPlus, faEdit, faHeart, faFireAlt
};
},
methods: {
create() {
this.$router.push(`/my/pages/new`);
this.$router.push(`/pages/new`);
}
}
});

View File

@@ -94,6 +94,7 @@ import { url } from '@/config';
import MkButton from '@/components/ui/button.vue';
import { userPage } from '@/filters/user';
import * as os from '@/os';
import * as sound from '@/scripts/sound';
export default defineComponent({
components: {
@@ -245,11 +246,7 @@ export default defineComponent({
this.o.put(this.myColor, pos);
// サウンドを再生する
if (this.$store.state.device.enableSounds) {
const sound = new Audio(`${url}/assets/reversi-put-me.mp3`);
sound.volume = this.$store.state.device.soundVolume;
sound.play();
}
sound.play(this.myColor ? 'reversiPutBlack' : 'reversiPutWhite');
this.connection.send('set', {
pos: pos
@@ -268,10 +265,8 @@ export default defineComponent({
this.$forceUpdate();
// サウンドを再生する
if (this.$store.state.device.enableSounds && x.color != this.myColor) {
const sound = new Audio(`${url}/assets/reversi-put-you.mp3`);
sound.volume = this.$store.state.device.soundVolume;
sound.play();
if (x.color !== this.myColor) {
sound.play(x.color ? 'reversiPutBlack' : 'reversiPutWhite');
}
},

View File

@@ -75,14 +75,25 @@ import MkButton from '@/components/ui/button.vue';
import MkInfo from '@/components/ui/info.vue';
import MkInput from '@/components/ui/input.vue';
import MkSwitch from '@/components/ui/switch.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import * as os from '@/os';
export default defineComponent({
components: {
FormBase,
MkButton, MkInfo, MkInput, MkSwitch
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('twoStepAuthentication'),
icon: faLock
},
data: null,
supportsCredentials: !!navigator.credentials,
usePasswordLessLogin: this.$store.state.i.usePasswordLessLogin,
@@ -92,6 +103,7 @@ export default defineComponent({
faLock
};
},
methods: {
register() {
os.dialog({
@@ -225,6 +237,7 @@ export default defineComponent({
});
});
},
updatePasswordLessLogin() {
os.api('i/2fa/password-less', {
value: !!this.usePasswordLessLogin

View File

@@ -0,0 +1,185 @@
<template>
<FormBase>
<FormKeyValueView>
<template #key>ID</template>
<template #value><span class="_monospace">{{ $store.state.i.id }}</span></template>
</FormKeyValueView>
<FormGroup>
<FormKeyValueView>
<template #key>{{ $t('registeredDate') }}</template>
<template #value><MkTime :time="$store.state.i.createdAt" mode="detail"/></template>
</FormKeyValueView>
</FormGroup>
<FormGroup v-if="stats">
<template #label>{{ $t('statistics') }}</template>
<FormKeyValueView>
<template #key>{{ $t('notesCount') }}</template>
<template #value>{{ number(stats.notesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('repliesCount') }}</template>
<template #value>{{ number(stats.repliesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('renotesCount') }}</template>
<template #value>{{ number(stats.renotesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('repliedCount') }}</template>
<template #value>{{ number(stats.repliedCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('renotedCount') }}</template>
<template #value>{{ number(stats.renotedCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('pollVotesCount') }}</template>
<template #value>{{ number(stats.pollVotesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('pollVotedCount') }}</template>
<template #value>{{ number(stats.pollVotedCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('sentReactionsCount') }}</template>
<template #value>{{ number(stats.sentReactionsCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('receivedReactionsCount') }}</template>
<template #value>{{ number(stats.receivedReactionsCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('noteFavoritesCount') }}</template>
<template #value>{{ number(stats.noteFavoritesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followingCount') }}</template>
<template #value>{{ number(stats.followingCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followingCount') }} ({{ $t('local') }})</template>
<template #value>{{ number(stats.localFollowingCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followingCount') }} ({{ $t('remote') }})</template>
<template #value>{{ number(stats.remoteFollowingCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followersCount') }}</template>
<template #value>{{ number(stats.followersCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followersCount') }} ({{ $t('local') }})</template>
<template #value>{{ number(stats.localFollowersCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('followersCount') }} ({{ $t('remote') }})</template>
<template #value>{{ number(stats.remoteFollowersCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('pageLikesCount') }}</template>
<template #value>{{ number(stats.pageLikesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('pageLikedCount') }}</template>
<template #value>{{ number(stats.pageLikedCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('driveFilesCount') }}</template>
<template #value>{{ number(stats.driveFilesCount) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('driveUsage') }}</template>
<template #value>{{ bytes(stats.driveUsage) }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>{{ $t('reversiCount') }}</template>
<template #value>{{ number(stats.reversiCount) }}</template>
</FormKeyValueView>
</FormGroup>
<FormGroup>
<template #label>{{ $t('other') }}</template>
<FormKeyValueView>
<template #key>emailVerified</template>
<template #value>{{ $store.state.i.emailVerified ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>twoFactorEnabled</template>
<template #value>{{ $store.state.i.twoFactorEnabled ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>securityKeys</template>
<template #value>{{ $store.state.i.securityKeys ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>usePasswordLessLogin</template>
<template #value>{{ $store.state.i.usePasswordLessLogin ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>isModerator</template>
<template #value>{{ $store.state.i.isModerator ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
<FormKeyValueView>
<template #key>isAdmin</template>
<template #value>{{ $store.state.i.isAdmin ? $t('yes') : $t('no') }}</template>
</FormKeyValueView>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import FormKeyValueView from '@/components/form/key-value-view.vue';
import * as os from '@/os';
import number from '@/filters/number';
import bytes from '@/filters/bytes';
export default defineComponent({
components: {
FormBase,
FormSelect,
FormSwitch,
FormButton,
FormLink,
FormGroup,
FormKeyValueView,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('accountInfo'),
icon: faInfoCircle
},
stats: null
}
},
mounted() {
this.$emit('info', this.INFO);
os.api('users/stats', {
userId: this.$store.state.i.id
}).then(stats => {
this.stats = stats;
});
},
methods: {
number,
bytes,
}
});
</script>

View File

@@ -1,26 +1,27 @@
<template>
<div>
<div class="_section">
<div class="_content">
<MkButton @click="generateToken">{{ $t('generateAccessToken') }}</MkButton>
</div>
</div>
<div class="_section">
<MkA to="/api-console" :behavior="isDesktop ? 'window' : null">API console</MkA>
</div>
</div>
<FormBase>
<FormButton @click="generateToken" primary>{{ $t('generateAccessToken') }}</FormButton>
<FormLink to="/settings/apps">{{ $t('manageAccessTokens') }}</FormLink>
<FormLink to="/api-console" :behavior="isDesktop ? 'window' : null">API console</FormLink>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faKey } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton, MkInput
FormBase,
FormButton,
FormLink,
},
emits: ['info'],

View File

@@ -1,6 +1,6 @@
<template>
<div>
<MkPagination :pagination="pagination" class="bfomjevm" ref="list">
<FormBase>
<FormPagination :pagination="pagination" ref="list">
<template #empty>
<div class="_fullinfo">
<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
@@ -8,8 +8,8 @@
</div>
</template>
<template #default="{items}">
<div class="token _panel" v-for="token in items" :key="token.id">
<img class="icon" :src="token.iconUrl" alt=""/>
<div class="_formPanel bfomjevm" v-for="token in items" :key="token.id">
<img class="icon" :src="token.iconUrl" alt="" v-if="token.iconUrl"/>
<div class="body">
<div class="name">{{ token.name }}</div>
<div class="description">{{ token.description }}</div>
@@ -33,21 +33,29 @@
</div>
</div>
</template>
</MkPagination>
</div>
</FormPagination>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faTrashAlt, faPlug } from '@fortawesome/free-solid-svg-icons';
import MkPagination from '@/components/ui/pagination.vue';
import FormPagination from '@/components/form/pagination.vue';
import FormSelect from '@/components/form/select.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkPagination
FormBase,
FormPagination,
},
emits: ['info'],
data() {
return {
INFO: {
@@ -65,6 +73,10 @@ export default defineComponent({
};
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
revoke(token) {
os.api('i/revoke-token', { tokenId: token.id }).then(() => {
@@ -77,26 +89,24 @@ export default defineComponent({
<style lang="scss" scoped>
.bfomjevm {
> .token {
display: flex;
padding: 16px;
display: flex;
padding: 16px;
> .icon {
display: block;
flex-shrink: 0;
margin: 0 12px 0 0;
width: 50px;
height: 50px;
border-radius: 8px;
}
> .icon {
display: block;
flex-shrink: 0;
margin: 0 12px 0 0;
width: 50px;
height: 50px;
border-radius: 8px;
}
> .body {
width: calc(100% - 62px);
position: relative;
> .body {
width: calc(100% - 62px);
position: relative;
> .name {
font-weight: bold;
}
> .name {
font-weight: bold;
}
}
}

View File

@@ -0,0 +1,90 @@
<template>
<FormBase>
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faColumns"/> </div>
<div class="_content">
<div>{{ $t('defaultNavigationBehaviour') }}</div>
<MkSwitch v-model:value="deckNavWindow">{{ $t('openInWindow') }}</MkSwitch>
</div>
<div class="_content">
<MkSwitch v-model:value="deckAlwaysShowMainColumn">
{{ $t('_deck.alwaysShowMainColumn') }}
</MkSwitch>
</div>
<div class="_content">
<div>{{ $t('_deck.columnAlign') }}</div>
<MkRadio v-model="deckColumnAlign" value="left">{{ $t('left') }}</MkRadio>
<MkRadio v-model="deckColumnAlign" value="center">{{ $t('center') }}</MkRadio>
</div>
</section>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faImage, faCog, faColumns } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/ui/switch.vue';
import MkSelect from '@/components/ui/select.vue';
import MkRadio from '@/components/ui/radio.vue';
import MkRadios from '@/components/ui/radios.vue';
import MkRange from '@/components/ui/range.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import { clientDb, set } from '@/db';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSwitch,
MkSelect,
MkRadio,
MkRadios,
MkRange,
FormSwitch,
FormSelect,
FormRadios,
FormBase,
FormGroup,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('deck'),
icon: faColumns
},
faImage, faCog,
}
},
computed: {
deckNavWindow: {
get() { return this.$store.state.device.deckNavWindow; },
set(value) { this.$store.commit('device/set', { key: 'deckNavWindow', value }); }
},
deckAlwaysShowMainColumn: {
get() { return this.$store.state.device.deckAlwaysShowMainColumn; },
set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); }
},
deckColumnAlign: {
get() { return this.$store.state.device.deckColumnAlign; },
set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
},
},
mounted() {
this.$emit('info', this.INFO);
},
});
</script>

View File

@@ -0,0 +1,71 @@
<template>
<FormBase>
<FormGroup>
<FormInput v-model:value="emailAddress" type="email">
{{ $t('emailAddress') }}
<template #desc v-if="$store.state.i.email && !$store.state.i.emailVerified">{{ $t('verificationEmailSent') }}</template>
<template #desc v-else-if="emailAddress === $store.state.i.email && $store.state.i.emailVerified">{{ $t('emailVerified') }}</template>
</FormInput>
</FormGroup>
<FormButton @click="save" primary>{{ $t('save') }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCog } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import FormButton from '@/components/form/button.vue';
import FormInput from '@/components/form/input.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import * as os from '@/os';
export default defineComponent({
components: {
FormBase,
FormInput,
FormButton,
FormGroup,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('emailAddress'),
icon: faEnvelope
},
emailAddress: null,
code: null,
faCog
}
},
created() {
this.emailAddress = this.$store.state.i.email;
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
save() {
os.dialog({
title: this.$t('password'),
input: {
type: 'password'
}
}).then(({ canceled, result: password }) => {
if (canceled) return;
os.api('i/update-email', {
password: password,
email: this.emailAddress,
});
});
}
}
});
</script>

View File

@@ -0,0 +1,52 @@
<template>
<FormBase>
<FormGroup>
<template #label>{{ $t('emailAddress') }}</template>
<FormLink to="/settings/email/address">
<template v-if="$store.state.i.email && !$store.state.i.emailVerified" #icon><Fa :icon="faExclamationTriangle" style="color: var(--warn);"/></template>
<template v-else-if="$store.state.i.email && $store.state.i.emailVerified" #icon><Fa :icon="faCheck" style="color: var(--success);"/></template>
{{ $store.state.i.email || $t('notSet') }}
</FormLink>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCog, faExclamationTriangle, faCheck } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import FormButton from '@/components/form/button.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import * as os from '@/os';
export default defineComponent({
components: {
FormBase,
FormLink,
FormButton,
FormGroup,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('email'),
icon: faEnvelope
},
faCog, faExclamationTriangle, faCheck
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
}
});
</script>

View File

@@ -1,109 +1,110 @@
<template>
<div class="">
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faCog"/> {{ $t('general') }}</div>
<div class="_content">
<MkRadios v-model="serverDisconnectedBehavior">
<template #desc>{{ $t('whenServerDisconnected') }}</template>
<option value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</option>
<option value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</option>
<option value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</option>
</MkRadios>
<MkSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</MkSwitch>
<MkSwitch v-model:value="showFixedPostForm">{{ $t('showFixedPostForm') }}</MkSwitch>
<MkSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</MkSwitch>
<MkSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</MkSwitch>
<MkSelect v-model:value="lang">
<template #label>{{ $t('uiLanguage') }}</template>
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
</MkSelect>
</div>
</section>
<FormBase>
<FormSwitch v-model:value="showFixedPostForm">{{ $t('showFixedPostForm') }}</FormSwitch>
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faCog"/> {{ $t('defaultNavigationBehaviour') }}</div>
<div class="_content">
<MkSwitch v-model:value="defaultSideView">{{ $t('openInSideView') }}</MkSwitch>
</div>
<div class="_content">
<MkRadios v-model="chatOpenBehavior">
<template #desc>{{ $t('chatOpenBehavior') }}</template>
<option value="page">{{ $t('showInPage') }}</option>
<option value="window">{{ $t('openInWindow') }}</option>
<option value="popout">{{ $t('popout') }}</option>
</MkRadios>
</div>
</section>
<FormSelect v-model:value="lang">
<template #label>{{ $t('uiLanguage') }}</template>
<option v-for="x in langs" :value="x[0]" :key="x[0]">{{ x[1] }}</option>
<template #caption>
<i18n-t keypath="i18nInfo" tag="span">
<template #link>
<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
</template>
</i18n-t>
</template>
</FormSelect>
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faCog"/> {{ $t('appearance') }}</div>
<div class="_content">
<MkSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</MkSwitch>
<MkSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</MkSwitch>
<MkSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</MkSwitch>
<MkSwitch v-model:value="useOsNativeEmojis">
{{ $t('useOsNativeEmojis') }}
<template #desc><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></template>
</MkSwitch>
<MkRadios v-model="fontSize">
<template #desc>{{ $t('fontSize') }}</template>
<option value="small"><span style="font-size: 14px;">Aa</span></option>
<option :value="null"><span style="font-size: 16px;">Aa</span></option>
<option value="large"><span style="font-size: 18px;">Aa</span></option>
<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
</MkRadios>
<MkRadios v-model="instanceTicker">
<template #desc>{{ $t('instanceTicker') }}</template>
<option value="none">{{ $t('_instanceTicker.none') }}</option>
<option value="remote">{{ $t('_instanceTicker.remote') }}</option>
<option value="always">{{ $t('_instanceTicker.always') }}</option>
</MkRadios>
</div>
</section>
<FormGroup>
<template #label>{{ $t('behavior') }}</template>
<FormSwitch v-model:value="imageNewTab">{{ $t('openImageInNewTab') }}</FormSwitch>
<FormSwitch v-model:value="enableInfiniteScroll">{{ $t('enableInfiniteScroll') }}</FormSwitch>
<FormSwitch v-model:value="disablePagesScript">{{ $t('disablePagesScript') }}</FormSwitch>
</FormGroup>
<section class="_card _vMargin">
<div class="_title"><Fa :icon="faColumns"/> {{ $t('deck') }}</div>
<div class="_content">
<div>{{ $t('defaultNavigationBehaviour') }}</div>
<MkSwitch v-model:value="deckNavWindow">{{ $t('openInWindow') }}</MkSwitch>
</div>
<div class="_content">
<MkSwitch v-model:value="deckAlwaysShowMainColumn">
{{ $t('_deck.alwaysShowMainColumn') }}
</MkSwitch>
</div>
<div class="_content">
<div>{{ $t('_deck.columnAlign') }}</div>
<MkRadio v-model="deckColumnAlign" value="left">{{ $t('left') }}</MkRadio>
<MkRadio v-model="deckColumnAlign" value="center">{{ $t('center') }}</MkRadio>
</div>
</section>
<FormSelect v-model:value="serverDisconnectedBehavior">
<template #label>{{ $t('whenServerDisconnected') }}</template>
<option value="reload">{{ $t('_serverDisconnectedBehavior.reload') }}</option>
<option value="dialog">{{ $t('_serverDisconnectedBehavior.dialog') }}</option>
<option value="quiet">{{ $t('_serverDisconnectedBehavior.quiet') }}</option>
</FormSelect>
<MkButton @click="cacheClear()" primary style="margin: var(--margin) auto;">{{ $t('cacheClear') }}</MkButton>
</div>
<FormGroup>
<template #label>{{ $t('appearance') }}</template>
<FormSwitch v-model:value="disableAnimatedMfm">{{ $t('disableAnimatedMfm') }}</FormSwitch>
<FormSwitch v-model:value="reduceAnimation">{{ $t('reduceUiAnimation') }}</FormSwitch>
<FormSwitch v-model:value="useBlurEffectForModal">{{ $t('useBlurEffectForModal') }}</FormSwitch>
<FormSwitch v-model:value="useOsNativeEmojis">{{ $t('useOsNativeEmojis') }}
<div><Mfm text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
</FormSwitch>
<FormSwitch v-model:value="loadRawImages">{{ $t('loadRawImages') }}</FormSwitch>
<FormSwitch v-model:value="disableShowingAnimatedImages">{{ $t('disableShowingAnimatedImages') }}</FormSwitch>
</FormGroup>
<FormRadios v-model="fontSize">
<template #desc>{{ $t('fontSize') }}</template>
<option value="small"><span style="font-size: 14px;">Aa</span></option>
<option :value="null"><span style="font-size: 16px;">Aa</span></option>
<option value="large"><span style="font-size: 18px;">Aa</span></option>
<option value="veryLarge"><span style="font-size: 20px;">Aa</span></option>
</FormRadios>
<FormSelect v-model:value="instanceTicker">
<template #label>{{ $t('instanceTicker') }}</template>
<option value="none">{{ $t('_instanceTicker.none') }}</option>
<option value="remote">{{ $t('_instanceTicker.remote') }}</option>
<option value="always">{{ $t('_instanceTicker.always') }}</option>
</FormSelect>
<FormSelect v-model:value="nsfw">
<template #label>{{ $t('nsfw') }}</template>
<option value="respect">{{ $t('_nsfw.respect') }}</option>
<option value="ignore">{{ $t('_nsfw.ignore') }}</option>
<option value="force">{{ $t('_nsfw.force') }}</option>
</FormSelect>
<FormGroup>
<template #label>{{ $t('defaultNavigationBehaviour') }}</template>
<FormSwitch v-model:value="defaultSideView">{{ $t('openInSideView') }}</FormSwitch>
</FormGroup>
<FormSelect v-model:value="chatOpenBehavior">
<template #label>{{ $t('chatOpenBehavior') }}</template>
<option value="page">{{ $t('showInPage') }}</option>
<option value="window">{{ $t('openInWindow') }}</option>
<option value="popout">{{ $t('popout') }}</option>
</FormSelect>
<FormLink to="/settings/deck">{{ $t('deck') }}</FormLink>
<FormButton @click="cacheClear()" danger>{{ $t('cacheClear') }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faImage, faCog, faColumns, faCogs } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/ui/switch.vue';
import MkSelect from '@/components/ui/select.vue';
import MkRadio from '@/components/ui/radio.vue';
import MkRadios from '@/components/ui/radios.vue';
import MkRange from '@/components/ui/range.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/form/button.vue';
import MkLink from '@/components/link.vue';
import { langs } from '@/config';
import { clientDb, set } from '@/db';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSwitch,
MkSelect,
MkRadio,
MkRadios,
MkRange,
MkLink,
FormSwitch,
FormSelect,
FormRadios,
FormBase,
FormGroup,
FormLink,
FormButton,
},
emits: ['info'],
@@ -167,11 +168,6 @@ export default defineComponent({
set(value) { this.$store.commit('device/set', { key: 'defaultSideView', value }); }
},
deckNavWindow: {
get() { return this.$store.state.device.deckNavWindow; },
set(value) { this.$store.commit('device/set', { key: 'deckNavWindow', value }); }
},
chatOpenBehavior: {
get() { return this.$store.state.device.chatOpenBehavior; },
set(value) { this.$store.commit('device/set', { key: 'chatOpenBehavior', value }); }
@@ -182,20 +178,25 @@ export default defineComponent({
set(value) { this.$store.commit('device/set', { key: 'instanceTicker', value }); }
},
loadRawImages: {
get() { return this.$store.state.device.loadRawImages; },
set(value) { this.$store.commit('device/set', { key: 'loadRawImages', value }); }
},
disableShowingAnimatedImages: {
get() { return this.$store.state.device.disableShowingAnimatedImages; },
set(value) { this.$store.commit('device/set', { key: 'disableShowingAnimatedImages', value }); }
},
nsfw: {
get() { return this.$store.state.device.nsfw; },
set(value) { this.$store.commit('device/set', { key: 'nsfw', value }); }
},
enableInfiniteScroll: {
get() { return this.$store.state.device.enableInfiniteScroll; },
set(value) { this.$store.commit('device/set', { key: 'enableInfiniteScroll', value }); }
},
deckAlwaysShowMainColumn: {
get() { return this.$store.state.device.deckAlwaysShowMainColumn; },
set(value) { this.$store.commit('device/set', { key: 'deckAlwaysShowMainColumn', value }); }
},
deckColumnAlign: {
get() { return this.$store.state.device.deckColumnAlign; },
set(value) { this.$store.commit('device/set', { key: 'deckColumnAlign', value }); }
},
},
watch: {

View File

@@ -1,35 +1,36 @@
<template>
<div class="vvcocwet" :class="{ wide: !narrow }" ref="el">
<div class="nav" v-if="!narrow || page == null">
<div class="menu">
<div class="label">{{ $t('basicSettings') }}</div>
<MkA class="item" :class="{ active: page === 'profile' }" replace to="/settings/profile"><Fa :icon="faUser" fixed-width class="icon"/>{{ $t('profile') }}</MkA>
<MkA class="item" :class="{ active: page === 'privacy' }" replace to="/settings/privacy"><Fa :icon="faLockOpen" fixed-width class="icon"/>{{ $t('privacy') }}</MkA>
<MkA class="item" :class="{ active: page === 'reaction' }" replace to="/settings/reaction"><Fa :icon="faLaugh" fixed-width class="icon"/>{{ $t('reaction') }}</MkA>
<MkA class="item" :class="{ active: page === 'notifications' }" replace to="/settings/notifications"><Fa :icon="faBell" fixed-width class="icon"/>{{ $t('notifications') }}</MkA>
<MkA class="item" :class="{ active: page === 'integration' }" replace to="/settings/integration"><Fa :icon="faShareAlt" fixed-width class="icon"/>{{ $t('integration') }}</MkA>
<MkA class="item" :class="{ active: page === 'security' }" replace to="/settings/security"><Fa :icon="faLock" fixed-width class="icon"/>{{ $t('security') }}</MkA>
</div>
<div class="menu">
<div class="label">{{ $t('clientSettings') }}</div>
<MkA class="item" :class="{ active: page === 'general' }" replace to="/settings/general"><Fa :icon="faCogs" fixed-width class="icon"/>{{ $t('general') }}</MkA>
<MkA class="item" :class="{ active: page === 'theme' }" replace to="/settings/theme"><Fa :icon="faPalette" fixed-width class="icon"/>{{ $t('theme') }}</MkA>
<MkA class="item" :class="{ active: page === 'sidebar' }" replace to="/settings/sidebar"><Fa :icon="faListUl" fixed-width class="icon"/>{{ $t('sidebar') }}</MkA>
<MkA class="item" :class="{ active: page === 'sounds' }" replace to="/settings/sounds"><Fa :icon="faMusic" fixed-width class="icon"/>{{ $t('sounds') }}</MkA>
<MkA class="item" :class="{ active: page === 'plugins' }" replace to="/settings/plugins"><Fa :icon="faPlug" fixed-width class="icon"/>{{ $t('plugins') }}</MkA>
</div>
<div class="menu">
<div class="label">{{ $t('otherSettings') }}</div>
<MkA class="item" :class="{ active: page === 'import-export' }" replace to="/settings/import-export"><Fa :icon="faBoxes" fixed-width class="icon"/>{{ $t('importAndExport') }}</MkA>
<MkA class="item" :class="{ active: page === 'mute-block' }" replace to="/settings/mute-block"><Fa :icon="faBan" fixed-width class="icon"/>{{ $t('muteAndBlock') }}</MkA>
<MkA class="item" :class="{ active: page === 'word-mute' }" replace to="/settings/word-mute"><Fa :icon="faCommentSlash" fixed-width class="icon"/>{{ $t('wordMute') }}</MkA>
<MkA class="item" :class="{ active: page === 'api' }" replace to="/settings/api"><Fa :icon="faKey" fixed-width class="icon"/>API</MkA>
<MkA class="item" :class="{ active: page === 'other' }" replace to="/settings/other"><Fa :icon="faEllipsisH" fixed-width class="icon"/>{{ $t('other') }}</MkA>
</div>
<div class="menu">
<button class="_button item" @click="logout">{{ $t('logout') }}</button>
</div>
</div>
<FormBase class="nav" v-if="!narrow || page == null" :force-wide="!narrow">
<FormGroup>
<template #label>{{ $t('basicSettings') }}</template>
<FormLink :active="page === 'profile'" replace to="/settings/profile"><template #icon><Fa :icon="faUser"/></template>{{ $t('profile') }}</FormLink>
<FormLink :active="page === 'privacy'" replace to="/settings/privacy"><template #icon><Fa :icon="faLockOpen"/></template>{{ $t('privacy') }}</FormLink>
<FormLink :active="page === 'reaction'" replace to="/settings/reaction"><template #icon><Fa :icon="faLaugh"/></template>{{ $t('reaction') }}</FormLink>
<FormLink :active="page === 'notifications'" replace to="/settings/notifications"><template #icon><Fa :icon="faBell"/></template>{{ $t('notifications') }}</FormLink>
<FormLink :active="page === 'email'" replace to="/settings/email"><template #icon><Fa :icon="faEnvelope"/></template>{{ $t('email') }}</FormLink>
<FormLink :active="page === 'integration'" replace to="/settings/integration"><template #icon><Fa :icon="faShareAlt"/></template>{{ $t('integration') }}</FormLink>
<FormLink :active="page === 'security'" replace to="/settings/security"><template #icon><Fa :icon="faLock"/></template>{{ $t('security') }}</FormLink>
</FormGroup>
<FormGroup>
<template #label>{{ $t('clientSettings') }}</template>
<FormLink :active="page === 'general'" replace to="/settings/general"><template #icon><Fa :icon="faCogs"/></template>{{ $t('general') }}</FormLink>
<FormLink :active="page === 'theme'" replace to="/settings/theme"><template #icon><Fa :icon="faPalette"/></template>{{ $t('theme') }}</FormLink>
<FormLink :active="page === 'sidebar'" replace to="/settings/sidebar"><template #icon><Fa :icon="faListUl"/></template>{{ $t('sidebar') }}</FormLink>
<FormLink :active="page === 'sounds'" replace to="/settings/sounds"><template #icon><Fa :icon="faMusic"/></template>{{ $t('sounds') }}</FormLink>
<FormLink :active="page === 'plugins'" replace to="/settings/plugins"><template #icon><Fa :icon="faPlug"/></template>{{ $t('plugins') }}</FormLink>
</FormGroup>
<FormGroup>
<template #label>{{ $t('otherSettings') }}</template>
<FormLink :active="page === 'import-export'" replace to="/settings/import-export"><template #icon><Fa :icon="faBoxes"/></template>{{ $t('importAndExport') }}</FormLink>
<FormLink :active="page === 'mute-block'" replace to="/settings/mute-block"><template #icon><Fa :icon="faBan"/></template>{{ $t('muteAndBlock') }}</FormLink>
<FormLink :active="page === 'word-mute'" replace to="/settings/word-mute"><template #icon><Fa :icon="faCommentSlash"/></template>{{ $t('wordMute') }}</FormLink>
<FormLink :active="page === 'api'" replace to="/settings/api"><template #icon><Fa :icon="faKey"/></template>API</FormLink>
<FormLink :active="page === 'other'" replace to="/settings/other"><template #icon><Fa :icon="faEllipsisH"/></template>{{ $t('other') }}</FormLink>
</FormGroup>
<FormGroup>
<FormButton @click="logout" danger>{{ $t('logout') }}</FormButton>
</FormGroup>
</FormBase>
<div class="main">
<component :is="component" @info="onInfo"/>
</div>
@@ -37,13 +38,25 @@
</template>
<script lang="ts">
import { computed, defineAsyncComponent, defineComponent, onMounted, ref } from 'vue';
import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, ref, watch } from 'vue';
import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes } from '@fortawesome/free-solid-svg-icons';
import { faLaugh, faBell } from '@fortawesome/free-regular-svg-icons';
import { faLaugh, faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { store } from '@/store';
import { i18n } from '@/i18n';
import FormLink from '@/components/form/link.vue';
import FormGroup from '@/components/form/group.vue';
import FormBase from '@/components/form/base.vue';
import FormButton from '@/components/form/button.vue';
import { scroll } from '../../scripts/scroll';
export default defineComponent({
components: {
FormBase,
FormLink,
FormGroup,
FormButton,
},
props: {
page: {
type: String,
@@ -72,21 +85,35 @@ export default defineComponent({
case 'word-mute': return defineAsyncComponent(() => import('./word-mute.vue'));
case 'integration': return defineAsyncComponent(() => import('./integration.vue'));
case 'security': return defineAsyncComponent(() => import('./security.vue'));
case '2fa': return defineAsyncComponent(() => import('./2fa.vue'));
case 'api': return defineAsyncComponent(() => import('./api.vue'));
case 'apps': return defineAsyncComponent(() => import('./apps.vue'));
case 'other': return defineAsyncComponent(() => import('./other.vue'));
case 'general': return defineAsyncComponent(() => import('./general.vue'));
case 'email': return defineAsyncComponent(() => import('./email.vue'));
case 'email/address': return defineAsyncComponent(() => import('./email-address.vue'));
case 'theme': return defineAsyncComponent(() => import('./theme.vue'));
case 'theme/install': return defineAsyncComponent(() => import('./theme.install.vue'));
case 'theme/manage': return defineAsyncComponent(() => import('./theme.manage.vue'));
case 'sidebar': return defineAsyncComponent(() => import('./sidebar.vue'));
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
case 'plugins': return defineAsyncComponent(() => import('./plugins.vue'));
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
case 'regedit': return defineAsyncComponent(() => import('./regedit.vue'));
default: return null;
}
});
watch(component, () => {
nextTick(() => {
scroll(el.value, 0);
});
});
onMounted(() => {
narrow.value = el.value.offsetWidth < 650;
narrow.value = el.value.offsetWidth < 1025;
});
return {
@@ -100,7 +127,7 @@ export default defineComponent({
store.dispatch('logout');
location.href = '/';
},
faPalette, faPlug, faUser, faListUl, faLock, faLaugh, faCommentSlash, faMusic, faBell, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes,
faPalette, faPlug, faUser, faListUl, faLock, faLaugh, faCommentSlash, faMusic, faBell, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes, faEnvelope,
};
},
});
@@ -108,63 +135,19 @@ export default defineComponent({
<style lang="scss" scoped>
.vvcocwet {
> .nav {
> .menu {
margin: 16px 0;
> .label {
padding: 8px 32px;
font-size: 80%;
opacity: 0.7;
}
> .item {
display: block;
width: 100%;
box-sizing: border-box;
padding: 0 32px;
line-height: 40px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
//background: var(--panel);
//border-bottom: solid 1px var(--divider);
transition: padding 0.2s ease, color 0.1s ease;
&:first-of-type {
//border-top: solid 1px var(--divider);
}
&.active {
color: var(--accent);
padding-left: 42px;
}
&:hover {
text-decoration: none;
padding-left: 42px;
}
> .icon {
margin-right: 0.5em;
}
}
}
}
&.wide {
display: flex;
max-width: 1100px;
margin: 0 auto;
> .nav {
width: 30%;
max-width: 300px;
font-size: 0.95em;
border-right: solid 1px var(--divider);
width: 32%;
box-sizing: border-box;
border-right: solid 0.5px var(--divider);
}
> .main {
flex: 1;
padding: 32px;
--baseContentWidth: 100%;
::v-deep(._section) {

View File

@@ -1,6 +1,9 @@
<template>
<section class="rrfwjxfl _section">
<MkTab v-model:value="tab" :items="[{ label: $t('mutedUsers'), value: 'mute' }, { label: $t('blockedUsers'), value: 'block' }]" style="margin-bottom: var(--margin);"/>
<MkTab v-model:value="tab" style="margin-bottom: var(--margin);">
<option value="mute">{{ $t('mutedUsers') }}</option>
<option value="block">{{ $t('blockedUsers') }}</option>
</MkTab>
<div class="_content" v-if="tab === 'mute'">
<MkPagination :pagination="mutingPagination" class="muting">
<template #empty><MkInfo>{{ $t('noUsers') }}</MkInfo></template>

View File

@@ -1,29 +1,31 @@
<template>
<div>
<div class="_section">
<MkButton full primary @click="configure"><Fa :icon="faCog"/> {{ $t('notificationSetting') }}</MkButton>
</div>
<div class="_section">
<MkButton full @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</MkButton>
<MkButton full @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</MkButton>
<MkButton full @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</MkButton>
</div>
</div>
<FormBase>
<FormLink @click="configure">{{ $t('notificationSetting') }}</FormLink>
<FormGroup>
<FormButton @click="readAllNotifications">{{ $t('markAsReadAllNotifications') }}</FormButton>
<FormButton @click="readAllUnreadNotes">{{ $t('markAsReadAllUnreadNotes') }}</FormButton>
<FormButton @click="readAllMessagingMessages">{{ $t('markAsReadAllTalkMessages') }}</FormButton>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCog } from '@fortawesome/free-solid-svg-icons';
import { faBell } from '@fortawesome/free-regular-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/ui/switch.vue';
import FormButton from '@/components/form/button.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import { notificationTypes } from '../../../types';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSwitch,
FormBase,
FormLink,
FormButton,
FormGroup,
},
emits: ['info'],

View File

@@ -1,40 +1,43 @@
<template>
<div>
<div class="_section">
<div class="_card">
<div class="_content">
<MkSwitch v-model:value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
{{ $t('showFeaturedNotesInTimeline') }}
</MkSwitch>
</div>
</div>
</div>
<div class="_section">
<MkSwitch v-model:value="debug" @update:value="changeDebug">
<FormBase>
<FormSwitch :value="$store.state.i.injectFeaturedNote" @update:value="onChangeInjectFeaturedNote">
{{ $t('showFeaturedNotesInTimeline') }}
</FormSwitch>
<FormLink to="/settings/account-info">{{ $t('accountInfo') }}</FormLink>
<FormGroup>
<FormSwitch v-model:value="debug" @update:value="changeDebug">
DEBUG MODE
</MkSwitch>
<div v-if="debug">
<MkA to="/settings/regedit">RegEdit</MkA>
<MkButton @click="taskmanager">Task Manager</MkButton>
</div>
</div>
</div>
</FormSwitch>
<template v-if="debug">
<FormLink to="/settings/regedit">RegEdit</FormLink>
<FormButton @click="taskmanager">Task Manager</FormButton>
</template>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '@/components/ui/select.vue';
import MkSwitch from '@/components/ui/switch.vue';
import MkButton from '@/components/ui/button.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormLink from '@/components/form/link.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import * as os from '@/os';
import { debug } from '@/config';
export default defineComponent({
components: {
MkSelect,
MkSwitch,
MkButton,
FormBase,
FormSelect,
FormSwitch,
FormButton,
FormLink,
FormGroup,
},
emits: ['info'],

View File

@@ -1,36 +1,43 @@
<template>
<div class="_section">
<div class="_card">
<div class="_content">
<MkSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</MkSwitch>
<MkSwitch v-model:value="autoAcceptFollowed" v-if="isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</MkSwitch>
</div>
<div class="_content">
<MkSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</MkSwitch>
<MkSelect v-model:value="defaultNoteVisibility" style="margin-bottom: 8px;" v-if="!rememberNoteVisibility">
<template #label>{{ $t('defaultNoteVisibility') }}</template>
<option value="public">{{ $t('_visibility.public') }}</option>
<option value="home">{{ $t('_visibility.home') }}</option>
<option value="followers">{{ $t('_visibility.followers') }}</option>
<option value="specified">{{ $t('_visibility.specified') }}</option>
</MkSelect>
<MkSwitch v-model:value="defaultNoteLocalOnly" v-if="!rememberNoteVisibility">{{ $t('_visibility.localOnly') }}</MkSwitch>
</div>
</div>
</div>
<FormBase>
<FormGroup>
<FormSwitch v-model:value="isLocked" @update:value="save()">{{ $t('makeFollowManuallyApprove') }}</FormSwitch>
<FormSwitch v-model:value="autoAcceptFollowed" :disabled="!isLocked" @update:value="save()">{{ $t('autoAcceptFollowed') }}</FormSwitch>
<template #caption>{{ $t('lockedAccountInfo') }}</template>
</FormGroup>
<FormSwitch v-model:value="noCrawle" @update:value="save()">
{{ $t('noCrawle') }}
<template #desc>{{ $t('noCrawleDescription') }}</template>
</FormSwitch>
<FormSwitch v-model:value="rememberNoteVisibility" @update:value="save()">{{ $t('rememberNoteVisibility') }}</FormSwitch>
<FormGroup v-if="!rememberNoteVisibility">
<template #label>{{ $t('defaultNoteVisibility') }}</template>
<FormSelect v-model:value="defaultNoteVisibility">
<option value="public">{{ $t('_visibility.public') }}</option>
<option value="home">{{ $t('_visibility.home') }}</option>
<option value="followers">{{ $t('_visibility.followers') }}</option>
<option value="specified">{{ $t('_visibility.specified') }}</option>
</FormSelect>
<FormSwitch v-model:value="defaultNoteLocalOnly">{{ $t('_visibility.localOnly') }}</FormSwitch>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faLockOpen } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '@/components/ui/select.vue';
import MkSwitch from '@/components/ui/switch.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkSelect,
MkSwitch,
FormBase,
FormSelect,
FormGroup,
FormSwitch,
},
emits: ['info'],
@@ -43,6 +50,7 @@ export default defineComponent({
},
isLocked: false,
autoAcceptFollowed: false,
noCrawle: false,
}
},
@@ -66,6 +74,7 @@ export default defineComponent({
created() {
this.isLocked = this.$store.state.i.isLocked;
this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
this.noCrawle = this.$store.state.i.noCrawle;
},
mounted() {
@@ -77,6 +86,7 @@ export default defineComponent({
os.api('i/update', {
isLocked: !!this.isLocked,
autoAcceptFollowed: !!this.autoAcceptFollowed,
noCrawle: !!this.noCrawle,
});
}
}

View File

@@ -1,79 +1,67 @@
<template>
<div class="_section">
<div class="llvierxe _card">
<div class="_title"><Fa :icon="faUser"/> {{ $t('profile') }}<small style="display: block; font-weight: normal; opacity: 0.6;">@{{ $store.state.i.username }}@{{ host }}</small></div>
<div class="_content">
<div class="header" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner">
<MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/>
</div>
<MkInput v-model:value="name" :max="30">
<span>{{ $t('_profile.name') }}</span>
</MkInput>
<MkTextarea v-model:value="description" :max="500">
<span>{{ $t('_profile.description') }}</span>
<template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
</MkTextarea>
<MkInput v-model:value="location">
<span>{{ $t('location') }}</span>
<template #prefix><Fa :icon="faMapMarkerAlt"/></template>
</MkInput>
<MkInput v-model:value="birthday" type="date">
<template #title>{{ $t('birthday') }}</template>
<template #prefix><Fa :icon="faBirthdayCake"/></template>
</MkInput>
<details class="fields">
<summary>{{ $t('_profile.metadata') }}</summary>
<div class="row">
<MkInput v-model:value="fieldName0">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue0">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
<div class="row">
<MkInput v-model:value="fieldName1">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue1">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
<div class="row">
<MkInput v-model:value="fieldName2">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue2">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
<div class="row">
<MkInput v-model:value="fieldName3">{{ $t('_profile.metadataLabel') }}</MkInput>
<MkInput v-model:value="fieldValue3">{{ $t('_profile.metadataContent') }}</MkInput>
</div>
</details>
<MkSwitch v-model:value="isBot">{{ $t('flagAsBot') }}</MkSwitch>
<MkSwitch v-model:value="isCat">{{ $t('flagAsCat') }}</MkSwitch>
</div>
<div class="_footer">
<MkButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
<FormBase class="llvierxe">
<div class="header _formItem" :style="{ backgroundImage: $store.state.i.bannerUrl ? `url(${ $store.state.i.bannerUrl })` : null }" @click="changeBanner">
<MkAvatar class="avatar" :user="$store.state.i" :disable-preview="true" :disable-link="true" @click.stop="changeAvatar"/>
</div>
</div>
<FormInput v-model:value="name" :max="30">
<span>{{ $t('_profile.name') }}</span>
</FormInput>
<FormTextarea v-model:value="description" :max="500">
<span>{{ $t('_profile.description') }}</span>
<template #desc>{{ $t('_profile.youCanIncludeHashtags') }}</template>
</FormTextarea>
<FormInput v-model:value="location">
<span>{{ $t('location') }}</span>
<template #prefix><Fa :icon="faMapMarkerAlt"/></template>
</FormInput>
<FormInput v-model:value="birthday" type="date">
<span>{{ $t('birthday') }}</span>
<template #prefix><Fa :icon="faBirthdayCake"/></template>
</FormInput>
<FormGroup>
<FormButton @click="editMetadata" primary>{{ $t('_profile.metadataEdit') }}</FormButton>
<template #caption>{{ $t('_profile.metadataDescription') }}</template>
</FormGroup>
<FormSwitch v-model:value="isCat">{{ $t('flagAsCat') }}<template #desc>{{ $t('flagAsCatDescription') }}</template></FormSwitch>
<FormSwitch v-model:value="isBot">{{ $t('flagAsBot') }}<template #desc>{{ $t('flagAsBotDescription') }}</template></FormSwitch>
<FormSwitch v-model:value="alwaysMarkNsfw">{{ $t('alwaysMarkSensitive') }}</FormSwitch>
<FormButton @click="save(true)" primary><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake } from '@fortawesome/free-solid-svg-icons';
import { faSave } from '@fortawesome/free-regular-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkInput from '@/components/ui/input.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkSwitch from '@/components/ui/switch.vue';
import FormButton from '@/components/form/button.vue';
import FormInput from '@/components/form/input.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormTuple from '@/components/form/tuple.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import { host } from '@/config';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkInput,
MkTextarea,
MkSwitch,
FormButton,
FormInput,
FormTextarea,
FormSwitch,
FormTuple,
FormBase,
FormGroup,
},
emits: ['info'],
@@ -101,6 +89,7 @@ export default defineComponent({
bannerId: null,
isBot: false,
isCat: false,
alwaysMarkNsfw: false,
saving: false,
faSave, faUnlockAlt, faCogs, faUser, faMapMarkerAlt, faBirthdayCake
}
@@ -115,6 +104,7 @@ export default defineComponent({
this.bannerId = this.$store.state.i.bannerId;
this.isBot = this.$store.state.i.isBot;
this.isCat = this.$store.state.i.isCat;
this.alwaysMarkNsfw = this.$store.state.i.alwaysMarkNsfw;
this.fieldName0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].name : null;
this.fieldValue0 = this.$store.state.i.fields[0] ? this.$store.state.i.fields[0].value : null;
@@ -147,7 +137,60 @@ export default defineComponent({
});
},
save(notify) {
async editMetadata() {
const { canceled, result } = await os.form(this.$t('_profile.metadata'), {
fieldName0: {
type: 'string',
label: this.$t('_profile.metadataLabel') + ' 1',
default: this.fieldName0,
},
fieldValue0: {
type: 'string',
label: this.$t('_profile.metadataContent') + ' 1',
default: this.fieldValue0,
},
fieldName1: {
type: 'string',
label: this.$t('_profile.metadataLabel') + ' 2',
default: this.fieldName1,
},
fieldValue1: {
type: 'string',
label: this.$t('_profile.metadataContent') + ' 2',
default: this.fieldValue1,
},
fieldName2: {
type: 'string',
label: this.$t('_profile.metadataLabel') + ' 3',
default: this.fieldName2,
},
fieldValue2: {
type: 'string',
label: this.$t('_profile.metadataContent') + ' 3',
default: this.fieldValue2,
},
fieldName3: {
type: 'string',
label: this.$t('_profile.metadataLabel') + ' 4',
default: this.fieldName3,
},
fieldValue3: {
type: 'string',
label: this.$t('_profile.metadataContent') + ' 4',
default: this.fieldValue3,
},
});
if (canceled) return;
this.fieldName0 = result.fieldName0;
this.fieldValue0 = result.fieldValue0;
this.fieldName1 = result.fieldName1;
this.fieldValue1 = result.fieldValue1;
this.fieldName2 = result.fieldName2;
this.fieldValue2 = result.fieldValue2;
this.fieldName3 = result.fieldName3;
this.fieldValue3 = result.fieldValue3;
const fields = [
{ name: this.fieldName0, value: this.fieldValue0 },
{ name: this.fieldName1, value: this.fieldValue1 },
@@ -155,6 +198,19 @@ export default defineComponent({
{ name: this.fieldName3, value: this.fieldValue3 },
];
os.api('i/update', {
fields,
}).then(i => {
os.success();
}).catch(err => {
os.dialog({
type: 'error',
text: err.id
});
});
},
save(notify) {
this.saving = true;
os.api('i/update', {
@@ -162,9 +218,9 @@ export default defineComponent({
description: this.description || null,
location: this.location || null,
birthday: this.birthday || null,
fields,
isBot: !!this.isBot,
isCat: !!this.isCat,
alwaysMarkNsfw: !!this.alwaysMarkNsfw,
}).then(i => {
this.saving = false;
this.$store.state.i.avatarId = i.avatarId;
@@ -189,41 +245,29 @@ export default defineComponent({
<style lang="scss" scoped>
.llvierxe {
> ._content {
> .header {
position: relative;
height: 150px;
overflow: hidden;
background-size: cover;
background-position: center;
border-radius: 5px;
border: solid 1px var(--divider);
box-sizing: border-box;
> .header {
position: relative;
height: 150px;
overflow: hidden;
background-size: cover;
background-position: center;
border-radius: 5px;
border: solid 1px var(--divider);
box-sizing: border-box;
cursor: pointer;
> .avatar {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: block;
width: 72px;
height: 72px;
margin: auto;
cursor: pointer;
> .avatar {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: block;
width: 72px;
height: 72px;
margin: auto;
cursor: pointer;
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
}
}
> .fields {
> .row {
> * {
display: inline-block;
width: 50%;
margin-bottom: 0;
}
}
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.5);
}
}
}

View File

@@ -1,9 +1,8 @@
<template>
<div class="_section">
<div class="_card">
<div class="_title"><Fa :icon="faLaugh"/> {{ $t('reaction') }}</div>
<div class="_content">
<div class="_caption" style="padding: 0 8px 8px 8px;">{{ $t('reactionSettingDescription') }}</div>
<FormBase>
<div class="_formItem">
<div class="_formLabel">{{ $t('reactionSettingDescription') }}</div>
<div class="_formPanel">
<XDraggable class="zoaiodol" :list="reactions" animation="150" delay="100" delay-on-touch-only="true">
<button class="_button item" v-for="reaction in reactions" :key="reaction" @click="remove(reaction, $event)">
<MkEmoji :emoji="reaction" :normal="true"/>
@@ -12,15 +11,25 @@
<button>a</button>
</template>
</XDraggable>
<div class="_caption" style="padding: 8px;">{{ $t('reactionSettingDescription2') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></div>
<MkSwitch v-model:value="useFullReactionPicker">{{ $t('useFullReactionPicker') }}</MkSwitch>
</div>
<div class="_footer">
<MkButton inline @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton>
<MkButton inline @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</MkButton>
</div>
<div class="_formCaption">{{ $t('reactionSettingDescription2') }} <button class="_textButton" @click="chooseEmoji">{{ $t('chooseEmoji') }}</button></div>
</div>
</div>
<FormRadios v-model="reactionPickerWidth">
<template #desc>{{ $t('width') }}</template>
<option :value="1">{{ $t('small') }}</option>
<option :value="2">{{ $t('medium') }}</option>
<option :value="3">{{ $t('large') }}</option>
</FormRadios>
<FormRadios v-model="reactionPickerHeight">
<template #desc>{{ $t('height') }}</template>
<option :value="1">{{ $t('small') }}</option>
<option :value="2">{{ $t('medium') }}</option>
<option :value="3">{{ $t('large') }}</option>
</FormRadios>
<FormButton @click="preview"><Fa :icon="faEye"/> {{ $t('preview') }}</FormButton>
<FormButton danger @click="setDefault"><Fa :icon="faUndo"/> {{ $t('default') }}</FormButton>
</FormBase>
</template>
<script lang="ts">
@@ -28,18 +37,19 @@ import { defineComponent } from 'vue';
import { faLaugh, faSave, faEye } from '@fortawesome/free-regular-svg-icons';
import { faUndo } from '@fortawesome/free-solid-svg-icons';
import { VueDraggableNext } from 'vue-draggable-next';
import MkInput from '@/components/ui/input.vue';
import MkButton from '@/components/ui/button.vue';
import MkSwitch from '@/components/ui/switch.vue';
import { emojiRegexWithCustom } from '../../../misc/emoji-regex';
import FormInput from '@/components/form/input.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormButton from '@/components/form/button.vue';
import { defaultSettings } from '@/store';
import * as os from '@/os';
export default defineComponent({
components: {
MkInput,
MkButton,
MkSwitch,
FormInput,
FormButton,
FormBase,
FormRadios,
XDraggable: VueDraggableNext,
},
@@ -49,7 +59,11 @@ export default defineComponent({
return {
INFO: {
title: this.$t('reaction'),
icon: faLaugh
icon: faLaugh,
action: {
icon: faEye,
handler: this.preview
}
},
reactions: JSON.parse(JSON.stringify(this.$store.state.settings.reactions)),
faLaugh, faSave, faEye, faUndo
@@ -61,6 +75,14 @@ export default defineComponent({
get() { return this.$store.state.device.useFullReactionPicker; },
set(value) { this.$store.commit('device/set', { key: 'useFullReactionPicker', value: value }); }
},
reactionPickerWidth: {
get() { return this.$store.state.device.reactionPickerWidth; },
set(value) { this.$store.commit('device/set', { key: 'reactionPickerWidth', value: value }); }
},
reactionPickerHeight: {
get() { return this.$store.state.device.reactionPickerHeight; },
set(value) { this.$store.commit('device/set', { key: 'reactionPickerHeight', value: value }); }
},
},
watch: {
@@ -92,7 +114,7 @@ export default defineComponent({
preview(ev) {
os.popup(import('@/components/emoji-picker.vue'), {
compact: !this.$store.state.device.useFullReactionPicker,
asReactionPicker: true,
src: ev.currentTarget || ev.target,
}, {}, 'closed');
},
@@ -123,8 +145,6 @@ export default defineComponent({
<style lang="scss" scoped>
.zoaiodol {
border: solid 1px var(--divider);
border-radius: var(--radius);
padding: 16px;
> .item {

View File

@@ -1,29 +1,45 @@
<template>
<div>
<div class="_section">
<X2fa/>
</div>
<div class="_section">
<MkButton primary @click="change()" full>{{ $t('changePassword') }}</MkButton>
</div>
<div class="_section">
<MkButton class="_vMargin" primary @click="regenerateToken" full><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</MkButton>
<div class="_caption _vMargin" style="padding: 0 6px;">{{ $t('regenerateLoginTokenDescription') }}</div>
</div>
</div>
<FormBase>
<X2fa/>
<FormLink to="/settings/2fa"><template #icon><Fa :icon="faMobileAlt"/></template>{{ $t('twoStepAuthentication') }}</FormLink>
<FormButton primary @click="change()">{{ $t('changePassword') }}</FormButton>
<FormPagination :pagination="pagination">
<template #label>{{ $t('signinHistory') }}</template>
<template #default="{items}">
<div class="_formPanel timnmucd" v-for="item in items" :key="item.id">
<header>
<Fa class="icon succ" :icon="faCheck" v-if="item.success"/>
<Fa class="icon fail" :icon="faTimesCircle" v-else/>
<code class="ip _monospace">{{ item.ip }}</code>
<MkTime :time="item.createdAt" class="time"/>
</header>
</div>
</template>
</FormPagination>
<FormGroup>
<FormButton danger @click="regenerateToken"><Fa :icon="faSyncAlt"/> {{ $t('regenerateLoginToken') }}</FormButton>
<template #caption>{{ $t('regenerateLoginTokenDescription') }}</template>
</FormGroup>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faLock, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import X2fa from './security.2fa.vue';
import { faCheck, faTimesCircle, faLock, faSyncAlt, faMobileAlt } from '@fortawesome/free-solid-svg-icons';
import FormBase from '@/components/form/base.vue';
import FormLink from '@/components/form/link.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import FormPagination from '@/components/form/pagination.vue';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
X2fa,
FormBase,
FormLink,
FormButton,
FormPagination,
FormGroup,
},
emits: ['info'],
@@ -34,7 +50,11 @@ export default defineComponent({
title: this.$t('security'),
icon: faLock
},
faLock, faSyncAlt
pagination: {
endpoint: 'i/signin-history',
limit: 5,
},
faLock, faSyncAlt, faCheck, faTimesCircle, faMobileAlt,
}
},
@@ -98,3 +118,32 @@ export default defineComponent({
}
});
</script>
<style lang="scss" scoped>
.timnmucd {
padding: 16px;
> header {
display: flex;
align-items: center;
> .icon {
width: 1em;
margin-right: 0.75em;
&.succ {
color: var(--success);
}
&.fail {
color: var(--error);
}
}
> .time {
margin-left: auto;
opacity: 0.7;
}
}
}
</style>

View File

@@ -1,41 +1,41 @@
<template>
<div class="_section">
<div class="_card">
<div class="_content">
<MkTextarea v-model:value="items" tall>
<span>{{ $t('sidebar') }}</span>
<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
</MkTextarea>
</div>
<div class="_content">
<div>{{ $t('display') }}</div>
<MkRadio v-model="sidebarDisplay" value="full">{{ $t('_sidebar.full') }}</MkRadio>
<MkRadio v-model="sidebarDisplay" value="icon">{{ $t('_sidebar.icon') }}</MkRadio>
<!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
</div>
<div class="_footer">
<MkButton inline @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
<MkButton inline @click="reset()"><Fa :icon="faRedo"/> {{ $t('default') }}</MkButton>
</div>
</div>
</div>
<FormBase>
<FormTextarea v-model:value="items" tall>
<span>{{ $t('sidebar') }}</span>
<template #desc><button class="_textButton" @click="addItem">{{ $t('addItem') }}</button></template>
</FormTextarea>
<FormRadios v-model="sidebarDisplay">
<template #desc>{{ $t('display') }}</template>
<option value="full">{{ $t('_sidebar.full') }}</option>
<option value="icon">{{ $t('_sidebar.icon') }}</option>
<!-- <MkRadio v-model="sidebarDisplay" value="hide" disabled>{{ $t('_sidebar.hide') }}</MkRadio>--> <!-- TODO: サイドバーを完全に隠せるようにすると別途ハンバーガーボタンのようなものをUIに表示する必要があり面倒 -->
</FormRadios>
<FormButton @click="save()" primary><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
<FormButton @click="reset()" danger><Fa :icon="faRedo"/> {{ $t('default') }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faListUl, faSave, faRedo } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import MkRadio from '@/components/ui/radio.vue';
import FormSwitch from '@/components/form/switch.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormButton from '@/components/form/button.vue';
import { defaultDeviceUserSettings } from '@/store';
import * as os from '@/os';
import { sidebarDef } from '@/sidebar';
export default defineComponent({
components: {
MkButton,
MkTextarea,
MkRadio,
FormBase,
FormButton,
FormTextarea,
FormRadios,
},
emits: ['info'],
@@ -102,7 +102,3 @@ export default defineComponent({
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -1,62 +1,35 @@
<template>
<div class="_section">
<div class="_card">
<div class="_title"><Fa :icon="faMusic"/> {{ $t('sounds') }}</div>
<div class="_content">
<MkRange v-model:value="sfxVolume" :min="0" :max="1" :step="0.1">
<Fa slot="icon" :icon="volumeIcon"/>
<span slot="title">{{ $t('volume') }}</span>
</MkRange>
</div>
<div class="_content">
<MkSelect v-model:value="sfxNote">
<template #label>{{ $t('_sfx.note') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxNote)" v-if="sfxNote"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxNoteMy">
<template #label>{{ $t('_sfx.noteMy') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxNoteMy)" v-if="sfxNoteMy"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxNotification">
<template #label>{{ $t('_sfx.notification') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxNotification)" v-if="sfxNotification"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxChat">
<template #label>{{ $t('_sfx.chat') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxChat)" v-if="sfxChat"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxChatBg">
<template #label>{{ $t('_sfx.chatBg') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxChatBg)" v-if="sfxChatBg"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxAntenna">
<template #label>{{ $t('_sfx.antenna') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxAntenna)" v-if="sfxAntenna"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
<MkSelect v-model:value="sfxChannel">
<template #label>{{ $t('_sfx.channel') }}</template>
<option v-for="sound in sounds" :value="sound" :key="sound">{{ sound || $t('none') }}</option>
<template #text><button class="_textButton" @click="listen(sfxChannel)" v-if="sfxChannel"><Fa :icon="faPlay"/> {{ $t('listen') }}</button></template>
</MkSelect>
</div>
</div>
</div>
<FormBase>
<FormRange v-model:value="masterVolume" :min="0" :max="1" :step="0.05">
<template #label><Fa :icon="volumeIcon" :key="volumeIcon"/> {{ $t('masterVolume') }}</template>
</FormRange>
<FormGroup>
<template #label>{{ $t('sounds') }}</template>
<FormButton v-for="type in Object.keys(sounds)" :key="type" :center="false" @click="edit(type)">
{{ $t('_sfx.' + type) }}
<template #suffix>{{ sounds[type].type || $t('none') }}</template>
<template #suffixIcon><Fa :icon="faChevronDown"/></template>
</FormButton>
</FormGroup>
<FormButton @click="reset()" danger><Fa :icon="faRedo"/> {{ $t('default') }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faMusic, faPlay, faVolumeUp, faVolumeMute } from '@fortawesome/free-solid-svg-icons';
import MkSelect from '@/components/ui/select.vue';
import MkRange from '@/components/ui/range.vue';
import { faMusic, faPlay, faVolumeUp, faVolumeMute, faChevronDown, faRedo } from '@fortawesome/free-solid-svg-icons';
import FormRange from '@/components/form/range.vue';
import FormSelect from '@/components/form/select.vue';
import FormBase from '@/components/form/base.vue';
import FormButton from '@/components/form/button.vue';
import FormGroup from '@/components/form/group.vue';
import * as os from '@/os';
import { device, defaultDeviceSettings } from '@/cold-storage';
import { playFile } from '@/scripts/sound';
const sounds = [
const soundsTypes = [
null,
'syuilo/up',
'syuilo/down',
@@ -73,6 +46,8 @@ const sounds = [
'syuilo/square-pico',
'syuilo/reverved',
'syuilo/ryukyu',
'syuilo/kick',
'syuilo/snare',
'aisha/1',
'aisha/2',
'aisha/3',
@@ -82,71 +57,98 @@ const sounds = [
export default defineComponent({
components: {
MkSelect,
MkRange,
FormSelect,
FormButton,
FormBase,
FormRange,
FormGroup,
},
emits: ['info'],
data() {
return {
sounds,
faMusic, faPlay, faVolumeUp, faVolumeMute,
INFO: {
title: this.$t('sounds'),
icon: faMusic
},
sounds: {},
faMusic, faPlay, faVolumeUp, faVolumeMute, faChevronDown, faRedo,
}
},
computed: {
sfxVolume: {
get() { return this.$store.state.device.sfxVolume; },
set(value) { this.$store.commit('device/set', { key: 'sfxVolume', value: parseFloat(value, 10) }); }
masterVolume: { // TODO: (外部)関数にcomputedを使うのはアレなので直す
get() { return device.get('sound_masterVolume'); },
set(value) { device.set('sound_masterVolume', value); }
},
sfxNote: {
get() { return this.$store.state.device.sfxNote; },
set(value) { this.$store.commit('device/set', { key: 'sfxNote', value }); }
},
sfxNoteMy: {
get() { return this.$store.state.device.sfxNoteMy; },
set(value) { this.$store.commit('device/set', { key: 'sfxNoteMy', value }); }
},
sfxNotification: {
get() { return this.$store.state.device.sfxNotification; },
set(value) { this.$store.commit('device/set', { key: 'sfxNotification', value }); }
},
sfxChat: {
get() { return this.$store.state.device.sfxChat; },
set(value) { this.$store.commit('device/set', { key: 'sfxChat', value }); }
},
sfxChatBg: {
get() { return this.$store.state.device.sfxChatBg; },
set(value) { this.$store.commit('device/set', { key: 'sfxChatBg', value }); }
},
sfxAntenna: {
get() { return this.$store.state.device.sfxAntenna; },
set(value) { this.$store.commit('device/set', { key: 'sfxAntenna', value }); }
},
sfxChannel: {
get() { return this.$store.state.device.sfxChannel; },
set(value) { this.$store.commit('device/set', { key: 'sfxChannel', value }); }
},
volumeIcon: {
get() {
return this.sfxVolume === 0 ? faVolumeMute : faVolumeUp;
}
volumeIcon() {
return this.masterVolume === 0 ? faVolumeMute : faVolumeUp;
}
},
created() {
this.sounds.note = device.get('sound_note');
this.sounds.noteMy = device.get('sound_noteMy');
this.sounds.notification = device.get('sound_notification');
this.sounds.chat = device.get('sound_chat');
this.sounds.chatBg = device.get('sound_chatBg');
this.sounds.antenna = device.get('sound_antenna');
this.sounds.channel = device.get('sound_channel');
this.sounds.reversiPutBlack = device.get('sound_reversiPutBlack');
this.sounds.reversiPutWhite = device.get('sound_reversiPutWhite');
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
listen(sound) {
const audio = new Audio(`/assets/sounds/${sound}.mp3`);
audio.volume = this.$store.state.device.sfxVolume;
audio.play();
async edit(type) {
const { canceled, result } = await os.form(this.$t('_sfx.' + type), {
type: {
type: 'enum',
enum: soundsTypes.map(x => ({
value: x,
label: x == null ? this.$t('none') : x,
})),
label: this.$t('sound'),
default: this.sounds[type].type,
},
volume: {
type: 'range',
mim: 0,
max: 1,
step: 0.05,
label: this.$t('volume'),
default: this.sounds[type].volume
},
listen: {
type: 'button',
content: this.$t('listen'),
action: (_, values) => {
playFile(values.type, values.volume);
}
}
});
if (canceled) return;
const v = {
type: result.type,
volume: result.volume,
};
device.set('sound_' + type, v);
this.sounds[type] = v;
},
reset() {
for (const sound of Object.keys(this.sounds)) {
const v = defaultDeviceSettings['sound_' + sound];
device.set('sound_' + sound, v);
this.sounds[sound] = v;
}
}
}
});
</script>

View File

@@ -0,0 +1,106 @@
<template>
<FormBase>
<FormGroup>
<FormTextarea v-model:value="installThemeCode">
<span>{{ $t('_theme.code') }}</span>
</FormTextarea>
<FormButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><Fa :icon="faEye"/> {{ $t('preview') }}</FormButton>
</FormGroup>
<FormButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><Fa :icon="faCheck"/> {{ $t('install') }}</FormButton>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5';
import FormTextarea from '@/components/form/textarea.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/form/button.vue';
import { applyTheme, validateTheme } from '@/scripts/theme';
import * as os from '@/os';
export default defineComponent({
components: {
FormTextarea,
FormSelect,
FormRadios,
FormBase,
FormGroup,
FormLink,
FormButton,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('_theme.install'),
icon: faDownload
},
installThemeCode: null,
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
}
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
parseThemeCode(code) {
let theme;
try {
theme = JSON5.parse(code);
} catch (e) {
os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (!validateTheme(theme)) {
os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
os.dialog({
type: 'info',
text: this.$t('_theme.alreadyInstalled')
});
return false;
}
return theme;
},
preview(code) {
const theme = this.parseThemeCode(code);
if (theme) applyTheme(theme, false);
},
install(code) {
const theme = this.parseThemeCode(code);
if (!theme) return;
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
os.dialog({
type: 'success',
text: this.$t('_theme.installed', { name: theme.name })
});
},
}
});
</script>

View File

@@ -0,0 +1,103 @@
<template>
<FormBase>
<FormSelect v-model:value="selectedThemeId">
<template #label>{{ $t('installedThemes') }}</template>
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
<optgroup :label="$t('builtinThemes')">
<option v-for="x in builtinThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</FormSelect>
<template v-if="selectedTheme">
<FormInput readonly :value="selectedTheme.author">
<span>{{ $t('author') }}</span>
</FormInput>
<FormTextarea readonly tall :value="selectedThemeCode">
<span>{{ $t('_theme.code') }}</span>
<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $t('copy') }}</button></template>
</FormTextarea>
<FormButton @click="uninstall()" danger v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><Fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</FormButton>
</template>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5';
import FormTextarea from '@/components/form/textarea.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormInput from '@/components/form/input.vue';
import FormButton from '@/components/form/button.vue';
import { Theme, builtinThemes } from '@/scripts/theme';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import * as os from '@/os';
export default defineComponent({
components: {
FormTextarea,
FormSelect,
FormRadios,
FormBase,
FormGroup,
FormInput,
FormButton,
},
emits: ['info'],
data() {
return {
INFO: {
title: this.$t('_theme.manage'),
icon: faFolderOpen
},
builtinThemes,
selectedThemeId: null,
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
}
},
computed: {
themes(): Theme[] {
return builtinThemes.concat(this.$store.state.device.themes);
},
installedThemes(): Theme[] {
return this.$store.state.device.themes;
},
selectedTheme() {
if (this.selectedThemeId == null) return null;
return this.themes.find(x => x.id === this.selectedThemeId);
},
selectedThemeCode() {
if (this.selectedTheme == null) return null;
return JSON5.stringify(this.selectedTheme, null, '\t');
},
},
mounted() {
this.$emit('info', this.INFO);
},
methods: {
copyThemeCode() {
copyToClipboard(this.selectedThemeCode);
os.success();
},
uninstall() {
const theme = this.selectedTheme;
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
os.success();
},
}
});
</script>

View File

@@ -1,7 +1,26 @@
<template>
<div class="">
<div class="rfqxtzch _card _vMargin">
<div class="_content">
<FormBase>
<FormSelect v-model:value="lightTheme" v-if="!darkMode">
<template #label>{{ $t('themeForLightMode') }}</template>
<optgroup :label="$t('lightThemes')">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
<optgroup :label="$t('darkThemes')">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</FormSelect>
<FormSelect v-model:value="darkTheme" v-else>
<template #label>{{ $t('themeForDarkMode') }}</template>
<optgroup :label="$t('darkThemes')">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
<optgroup :label="$t('lightThemes')">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</FormSelect>
<FormGroup>
<div class="rfqxtzch _formItem _formPanel">
<div class="darkMode" :class="{ disabled: syncDeviceDarkMode }">
<div class="toggleWrapper">
<input type="checkbox" class="dn" id="dn" v-model="darkMode" :disabled="syncDeviceDarkMode"/>
@@ -23,85 +42,47 @@
</div>
</div>
</div>
<div class="_content">
<MkSwitch v-model:value="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</MkSwitch>
</div>
</div>
<div class="_card _vMargin">
<div class="_content">
<MkSelect v-model:value="lightTheme">
<template #label>{{ $t('themeForLightMode') }}</template>
<optgroup :label="$t('lightThemes')">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
<optgroup :label="$t('darkThemes')">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</MkSelect>
<MkSelect v-model:value="darkTheme">
<template #label>{{ $t('themeForDarkMode') }}</template>
<optgroup :label="$t('darkThemes')">
<option v-for="x in darkThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
<optgroup :label="$t('lightThemes')">
<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</optgroup>
</MkSelect>
<a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank" class="_link">{{ $t('_theme.explore') }}</a><MkA to="/theme-editor" class="_link">{{ $t('_theme.make') }}</MkA>
</div>
<div class="_content">
<MkButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</MkButton>
<MkButton primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</MkButton>
</div>
</div>
<div class="_card _vMargin">
<div class="_title"><Fa :icon="faDownload"/> {{ $t('_theme.install') }}</div>
<div class="_content">
<MkTextarea v-model:value="installThemeCode">
<span>{{ $t('_theme.code') }}</span>
</MkTextarea>
<MkButton @click="() => install(installThemeCode)" :disabled="installThemeCode == null" primary inline><Fa :icon="faCheck"/> {{ $t('install') }}</MkButton>
<MkButton @click="() => preview(installThemeCode)" :disabled="installThemeCode == null" inline><Fa :icon="faEye"/> {{ $t('preview') }}</MkButton>
</div>
</div>
<div class="_card _vMargin">
<div class="_title"><Fa :icon="faFolderOpen"/> {{ $t('_theme.manage') }}</div>
<div class="_content">
<MkSelect v-model:value="selectedThemeId">
<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
</MkSelect>
<template v-if="selectedTheme">
<MkTextarea readonly tall :value="selectedThemeCode">
<span>{{ $t('_theme.code') }}</span>
<template #desc><button @click="copyThemeCode()" class="_textButton">{{ $t('copy') }}</button></template>
</MkTextarea>
<MkButton @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><Fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</MkButton>
</template>
</div>
</div>
</div>
<FormSwitch v-model:value="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</FormSwitch>
</FormGroup>
<FormButton primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</FormButton>
<FormButton primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</FormButton>
<FormGroup>
<FormLink to="https://assets.msky.cafe/theme/list" external>{{ $t('_theme.explore') }}</FormLink>
<FormLink to="/theme-editor">{{ $t('_theme.make') }}</FormLink>
</FormGroup>
<FormLink to="/settings/theme/install"><template #icon><Fa :icon="faDownload"/></template>{{ $t('_theme.install') }}</FormLink>
<FormLink to="/settings/theme/manage"><template #icon><Fa :icon="faFolderOpen"/></template>{{ $t('_theme.manage') }}</FormLink>
</FormBase>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye } from '@fortawesome/free-solid-svg-icons';
import * as JSON5 from 'json5';
import MkButton from '@/components/ui/button.vue';
import MkSelect from '@/components/ui/select.vue';
import MkSwitch from '@/components/ui/switch.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import { Theme, builtinThemes, applyTheme, validateTheme } from '@/scripts/theme';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormRadios from '@/components/form/radios.vue';
import FormBase from '@/components/form/base.vue';
import FormGroup from '@/components/form/group.vue';
import FormLink from '@/components/form/link.vue';
import FormButton from '@/components/form/button.vue';
import { Theme, builtinThemes, applyTheme } from '@/scripts/theme';
import { selectFile } from '@/scripts/select-file';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
MkSelect,
MkSwitch,
MkTextarea,
FormSwitch,
FormSelect,
FormRadios,
FormBase,
FormGroup,
FormLink,
FormButton,
},
emits: ['info'],
@@ -113,8 +94,6 @@ export default defineComponent({
icon: faPalette
},
builtinThemes,
installThemeCode: null,
selectedThemeId: null,
wallpaper: localStorage.getItem('wallpaper'),
faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt, faEye
}
@@ -156,16 +135,6 @@ export default defineComponent({
get() { return this.$store.state.device.syncDeviceDarkMode; },
set(value) { this.$store.commit('device/set', { key: 'syncDeviceDarkMode', value }); }
},
selectedTheme() {
if (this.selectedThemeId == null) return null;
return this.themes.find(x => x.id === this.selectedThemeId);
},
selectedThemeCode() {
if (this.selectedTheme == null) return null;
return JSON5.stringify(this.selectedTheme, null, '\t');
},
},
watch: {
@@ -207,292 +176,230 @@ export default defineComponent({
this.wallpaper = file.url;
});
},
copyThemeCode() {
copyToClipboard(this.selectedThemeCode);
os.success();
},
parseThemeCode(code) {
let theme;
try {
theme = JSON5.parse(code);
} catch (e) {
os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (!validateTheme(theme)) {
os.dialog({
type: 'error',
text: this.$t('_theme.invalid')
});
return false;
}
if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
os.dialog({
type: 'info',
text: this.$t('_theme.alreadyInstalled')
});
return false;
}
return theme;
},
preview(code) {
const theme = this.parseThemeCode(code);
if (theme) applyTheme(theme, false);
},
install(code) {
const theme = this.parseThemeCode(code);
if (!theme) return;
const themes = this.$store.state.device.themes.concat(theme);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
os.dialog({
type: 'success',
text: this.$t('_theme.installed', { name: theme.name })
});
},
uninstall() {
const theme = this.selectedTheme;
const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
this.$store.commit('device/set', {
key: 'themes', value: themes
});
os.success();
},
}
});
</script>
<style lang="scss" scoped>
.rfqxtzch {
> ._content {
> .darkMode {
position: relative;
padding: 32px 0;
padding: 16px;
&.disabled {
opacity: 0.7;
> .darkMode {
position: relative;
padding: 32px 0;
&, * {
cursor: not-allowed !important;
}
&.disabled {
opacity: 0.7;
&, * {
cursor: not-allowed !important;
}
}
.toggleWrapper {
.toggleWrapper {
position: absolute;
top: 50%;
left: 50%;
overflow: hidden;
padding: 0 100px;
transform: translate3d(-50%, -50%, 0);
input {
position: absolute;
top: 50%;
left: 50%;
overflow: hidden;
padding: 0 100px;
transform: translate3d(-50%, -50%, 0);
left: -99em;
}
}
input {
position: absolute;
left: -99em;
}
.toggle {
cursor: pointer;
display: inline-block;
position: relative;
width: 90px;
height: 50px;
background-color: #83D8FF;
border-radius: 90px - 6;
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
> .before, > .after {
position: absolute;
top: 15px;
font-size: 18px;
transition: color 1s ease;
}
.toggle {
cursor: pointer;
display: inline-block;
position: relative;
width: 90px;
height: 50px;
background-color: #83D8FF;
border-radius: 90px - 6;
transition: background-color 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
> .before {
left: -70px;
color: var(--accent);
}
> .before, > .after {
position: absolute;
top: 15px;
font-size: 18px;
transition: color 1s ease;
}
> .after {
right: -68px;
color: var(--fg);
}
}
.toggle__handler {
display: inline-block;
position: relative;
z-index: 1;
top: 3px;
left: 3px;
width: 50px - 6;
height: 50px - 6;
background-color: #FFCF96;
border-radius: 50px;
box-shadow: 0 2px 6px rgba(0,0,0,.3);
transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
transform: rotate(-45deg);
.crater {
position: absolute;
background-color: #E8CDA5;
opacity: 0;
transition: opacity 200ms ease-in-out !important;
border-radius: 100%;
}
.crater--1 {
top: 18px;
left: 10px;
width: 4px;
height: 4px;
}
.crater--2 {
top: 28px;
left: 22px;
width: 6px;
height: 6px;
}
.crater--3 {
top: 10px;
left: 25px;
width: 8px;
height: 8px;
}
}
.star {
position: absolute;
background-color: #ffffff;
transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
border-radius: 50%;
}
.star--1 {
top: 10px;
left: 35px;
z-index: 0;
width: 30px;
height: 3px;
}
.star--2 {
top: 18px;
left: 28px;
z-index: 1;
width: 30px;
height: 3px;
}
.star--3 {
top: 27px;
left: 40px;
z-index: 0;
width: 30px;
height: 3px;
}
.star--4,
.star--5,
.star--6 {
opacity: 0;
transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--4 {
top: 16px;
left: 11px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
.star--5 {
top: 32px;
left: 17px;
z-index: 0;
width: 3px;
height: 3px;
transform: translate3d(3px,0,0);
}
.star--6 {
top: 36px;
left: 28px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
input:checked {
+ .toggle {
background-color: #749DD6;
> .before {
left: -70px;
color: var(--accent);
color: var(--fg);
}
> .after {
right: -68px;
color: var(--fg);
}
}
.toggle__handler {
display: inline-block;
position: relative;
z-index: 1;
top: 3px;
left: 3px;
width: 50px - 6;
height: 50px - 6;
background-color: #FFCF96;
border-radius: 50px;
box-shadow: 0 2px 6px rgba(0,0,0,.3);
transition: all 400ms cubic-bezier(0.68, -0.55, 0.265, 1.55) !important;
transform: rotate(-45deg);
.crater {
position: absolute;
background-color: #E8CDA5;
opacity: 0;
transition: opacity 200ms ease-in-out !important;
border-radius: 100%;
color: var(--accent);
}
.crater--1 {
top: 18px;
left: 10px;
.toggle__handler {
background-color: #FFE5B5;
transform: translate3d(40px, 0, 0) rotate(0);
.crater { opacity: 1; }
}
.star--1 {
width: 2px;
height: 2px;
}
.star--2 {
width: 4px;
height: 4px;
transform: translate3d(-5px, 0, 0);
}
.crater--2 {
top: 28px;
left: 22px;
width: 6px;
height: 6px;
.star--3 {
width: 2px;
height: 2px;
transform: translate3d(-7px, 0, 0);
}
.crater--3 {
top: 10px;
left: 25px;
width: 8px;
height: 8px;
.star--4,
.star--5,
.star--6 {
opacity: 1;
transform: translate3d(0,0,0);
}
}
.star {
position: absolute;
background-color: #ffffff;
transition: all 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
border-radius: 50%;
}
.star--4 {
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--1 {
top: 10px;
left: 35px;
z-index: 0;
width: 30px;
height: 3px;
}
.star--5 {
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--2 {
top: 18px;
left: 28px;
z-index: 1;
width: 30px;
height: 3px;
}
.star--3 {
top: 27px;
left: 40px;
z-index: 0;
width: 30px;
height: 3px;
}
.star--4,
.star--5,
.star--6 {
opacity: 0;
transition: all 300ms 0 cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--4 {
top: 16px;
left: 11px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
.star--5 {
top: 32px;
left: 17px;
z-index: 0;
width: 3px;
height: 3px;
transform: translate3d(3px,0,0);
}
.star--6 {
top: 36px;
left: 28px;
z-index: 0;
width: 2px;
height: 2px;
transform: translate3d(3px,0,0);
}
input:checked {
+ .toggle {
background-color: #749DD6;
> .before {
color: var(--fg);
}
> .after {
color: var(--accent);
}
.toggle__handler {
background-color: #FFE5B5;
transform: translate3d(40px, 0, 0) rotate(0);
.crater { opacity: 1; }
}
.star--1 {
width: 2px;
height: 2px;
}
.star--2 {
width: 4px;
height: 4px;
transform: translate3d(-5px, 0, 0);
}
.star--3 {
width: 2px;
height: 2px;
transform: translate3d(-7px, 0, 0);
}
.star--4,
.star--5,
.star--6 {
opacity: 1;
transform: translate3d(0,0,0);
}
.star--4 {
transition: all 300ms 200ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--5 {
transition: all 300ms 300ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--6 {
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
.star--6 {
transition: all 300ms 400ms cubic-bezier(0.445, 0.05, 0.55, 0.95) !important;
}
}
}

View File

@@ -1,44 +1,53 @@
<template>
<div class="_section">
<div class="_card">
<MkTab v-model:value="tab" :items="[{ label: $t('_wordMute.soft'), value: 'soft' }, { label: $t('_wordMute.hard'), value: 'hard' }]"/>
<div class="_content">
<div>
<MkTab v-model:value="tab">
<option value="soft">{{ $t('_wordMute.soft') }}</option>
<option value="hard">{{ $t('_wordMute.hard') }}</option>
</MkTab>
<FormBase>
<div class="_formItem">
<div v-show="tab === 'soft'">
<MkInfo>{{ $t('_wordMute.softDescription') }}</MkInfo>
<MkTextarea v-model:value="softMutedWords">
<FormTextarea v-model:value="softMutedWords">
<span>{{ $t('_wordMute.muteWords') }}</span>
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
</MkTextarea>
</FormTextarea>
</div>
<div v-show="tab === 'hard'">
<MkInfo>{{ $t('_wordMute.hardDescription') }}</MkInfo>
<MkTextarea v-model:value="hardMutedWords" style="margin-bottom: 16px;">
<FormTextarea v-model:value="hardMutedWords">
<span>{{ $t('_wordMute.muteWords') }}</span>
<template #desc>{{ $t('_wordMute.muteWordsDescription') }}<br>{{ $t('_wordMute.muteWordsDescription2') }}</template>
</MkTextarea>
<div v-if="hardWordMutedNotesCount != null" class="_caption">{{ $t('_wordMute.mutedNotes') }}: {{ hardWordMutedNotesCount | number }}</div>
</FormTextarea>
<FormKeyValueView v-if="hardWordMutedNotesCount != null">
<template #key>{{ $t('_wordMute.mutedNotes') }}</template>
<template #value>{{ number(hardWordMutedNotesCount) }}</template>
</FormKeyValueView>
</div>
</div>
<div class="_footer">
<MkButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</MkButton>
</div>
</div>
<FormButton @click="save()" primary inline :disabled="!changed"><Fa :icon="faSave"/> {{ $t('save') }}</FormButton>
</FormBase>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faCommentSlash, faSave } from '@fortawesome/free-solid-svg-icons';
import MkButton from '@/components/ui/button.vue';
import MkTextarea from '@/components/ui/textarea.vue';
import FormTextarea from '@/components/form/textarea.vue';
import FormBase from '@/components/form/base.vue';
import FormKeyValueView from '@/components/form/key-value-view.vue';
import FormButton from '@/components/form/button.vue';
import MkTab from '@/components/tab.vue';
import MkInfo from '@/components/ui/info.vue';
import * as os from '@/os';
import number from '@/filters/number';
export default defineComponent({
components: {
MkButton,
MkTextarea,
FormBase,
FormButton,
FormTextarea,
FormKeyValueView,
MkTab,
MkInfo,
},
@@ -94,6 +103,8 @@ export default defineComponent({
});
this.changed = false;
},
number
}
});
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="_section">
<div>
<MkPagination :pagination="pagination" #default="{items}" class="mk-following-or-followers _content" ref="list">
<div class="users">
<MkUserInfo class="user" v-for="user in items.map(x => type === 'following' ? x.followee : x.follower)" :user="user" :key="user.id"/>

View File

@@ -1,15 +1,24 @@
<template>
<div>
<div ref="chart"></div>
</div>
<MkContainer>
<template #header><Fa :icon="faChartBar" style="margin-right: 0.5em;"/>{{ $t('activity') }}</template>
<div style="padding: 8px;">
<div ref="chart"></div>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import ApexCharts from 'apexcharts';
import { faChartBar } from '@fortawesome/free-solid-svg-icons';
import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
export default defineComponent({
components: {
MkContainer,
},
props: {
user: {
type: Object,
@@ -25,7 +34,8 @@ export default defineComponent({
return {
fetching: true,
data: [],
peak: null
peak: null,
faChartBar,
};
},
mounted() {

View File

@@ -1,29 +1,43 @@
<template>
<div class="ujigsodd">
<MkLoading v-if="fetching"/>
<div class="stream" v-if="!fetching && images.length > 0">
<MkA v-for="image in images"
class="img"
:style="`background-image: url(${thumbnail(image.file)})`"
:to="notePage(image.note)"
></MkA>
<MkContainer>
<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template>
<div class="ujigsodd">
<MkLoading v-if="fetching"/>
<div class="stream" v-if="!fetching && images.length > 0">
<MkA v-for="image in images"
class="img"
:style="`background-image: url(${thumbnail(image.file)})`"
:to="notePage(image.note)"
></MkA>
</div>
<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p>
</div>
<p class="empty" v-if="!fetching && images.length == 0">{{ $t('nothing') }}</p>
</div>
</MkContainer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faImage } from '@fortawesome/free-solid-svg-icons';
import { getStaticImageUrl } from '@/scripts/get-static-image-url';
import notePage from '../../filters/note';
import * as os from '@/os';
import MkContainer from '@/components/ui/container.vue';
export default defineComponent({
props: ['user'],
components: {
MkContainer,
},
props: {
user: {
type: Object,
required: true
},
},
data() {
return {
fetching: true,
images: []
images: [],
faImage
};
},
mounted() {
@@ -37,7 +51,7 @@ export default defineComponent({
os.api('users/notes', {
userId: this.user.id,
fileType: image,
excludeNsfw: !this.$store.state.device.alwaysShowNsfw,
excludeNsfw: this.$store.state.device.nsfw !== 'ignore',
limit: 9,
}).then(notes => {
for (const note of notes) {
@@ -66,6 +80,8 @@ export default defineComponent({
<style lang="scss" scoped>
.ujigsodd {
padding: 8px;
> .stream {
display: flex;
justify-content: center;

View File

@@ -1,115 +1,113 @@
<template>
<div class="mk-user-page" v-if="user" v-size="{ max: [500] }">
<!-- TODO -->
<!-- <div class="punished" v-if="user.isSuspended"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</div> -->
<!-- <div class="punished" v-if="user.isSilenced"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div> -->
<div>
<div class="mk-user-page" v-if="user" v-size="{ max: [500] }" :class="{ _section: narrow === false }">
<!-- TODO -->
<!-- <div class="punished" v-if="user.isSuspended"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSuspended') }}</div> -->
<!-- <div class="punished" v-if="user.isSilenced"><Fa :icon="faExclamationTriangle" style="margin-right: 8px;"/> {{ $t('userSilenced') }}</div> -->
<div class="profile _section _fitBottom">
<MkRemoteCaution v-if="user.host != null" :href="user.url" class="_content _vMargin"/>
<div class="main">
<div class="profile _vMargin" :class="{ _section: narrow === true }">
<MkRemoteCaution v-if="user.host != null" :href="user.url" class="_content _vMargin"/>
<div class="_content _vMargin" :key="user.id">
<div class="banner-container" :style="style">
<div class="banner" ref="banner" :style="style"></div>
<div class="fade"></div>
<div class="title">
<MkUserName class="name" :user="user" :nowrap="true"/>
<div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true" /></span>
<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
<span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
<div class="_content _panel _vMargin" :key="user.id">
<div class="banner-container" :style="style">
<div class="banner" ref="banner" :style="style"></div>
<div class="fade"></div>
<div class="title">
<MkUserName class="name" :user="user" :nowrap="true"/>
<div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true" /></span>
<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
<span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
</div>
</div>
<span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
<div class="actions" v-if="$store.getters.isSignedIn">
<button @click="menu" class="menu _button"><Fa :icon="faEllipsisH"/></button>
<MkFollowButton v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div>
</div>
<MkAvatar class="avatar" :user="user" :disable-preview="true"/>
<div class="title">
<MkUserName :user="user" :nowrap="false" class="name"/>
<div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true" /></span>
<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
<span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
</div>
</div>
<div class="description">
<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
<p v-else class="empty">{{ $t('noAccountDescription') }}</p>
</div>
<div class="fields system">
<dl class="field" v-if="user.location">
<dt class="name"><Fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt>
<dd class="value">{{ user.location }}</dd>
</dl>
<dl class="field" v-if="user.birthday">
<dt class="name"><Fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
</dl>
<dl class="field">
<dt class="name"><Fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
</dl>
</div>
<div class="fields" v-if="user.fields.length > 0">
<dl class="field" v-for="(field, i) in user.fields" :key="i">
<dt class="name">
<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
</dt>
<dd class="value">
<Mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
</dd>
</dl>
</div>
<div class="status">
<MkA :to="userPage(user)" :class="{ active: page === 'index' }">
<b>{{ number(user.notesCount) }}</b>
<span>{{ $t('notes') }}</span>
</MkA>
<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
<b>{{ number(user.followingCount) }}</b>
<span>{{ $t('following') }}</span>
</MkA>
<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
<b>{{ number(user.followersCount) }}</b>
<span>{{ $t('followers') }}</span>
</MkA>
</div>
</div>
<span class="followed" v-if="$store.getters.isSignedIn && $store.state.i.id != user.id && user.isFollowed">{{ $t('followsYou') }}</span>
<div class="actions" v-if="$store.getters.isSignedIn">
<button @click="menu" class="menu _button"><Fa :icon="faEllipsisH"/></button>
<MkFollowButton v-if="$store.state.i.id != user.id" :user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div>
<template v-if="page === 'index'">
<div v-if="user.pinnedNotes.length > 0" :class="{ _section: narrow === true, _vMargin: narrow === false }">
<XNote v-for="note in user.pinnedNotes" class="note _content _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/>
</div>
</div>
<MkAvatar class="avatar" :user="user" :disable-preview="true"/>
<div class="title">
<MkUserName :user="user" :nowrap="false" class="name"/>
<div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true" /></span>
<span v-if="user.isAdmin" :title="$t('isAdmin')" style="color: var(--badge);"><Fa :icon="faBookmark"/></span>
<span v-if="!user.isAdmin && user.isModerator" :title="$t('isModerator')" style="color: var(--badge);"><Fa :icon="farBookmark"/></span>
<span v-if="user.isLocked" :title="$t('isLocked')"><Fa :icon="faLock"/></span>
<span v-if="user.isBot" :title="$t('isBot')"><Fa :icon="faRobot"/></span>
<div v-if="narrow === true" class="_section">
<XPhotos class="_content _vMargin" :user="user" :key="user.id"/>
<XActivity class="_content _vMargin" :user="user" :key="user.id"/>
</div>
</div>
<div class="description">
<Mfm v-if="user.description" :text="user.description" :is-note="false" :author="user" :i="$store.state.i" :custom-emojis="user.emojis"/>
<p v-else class="empty">{{ $t('noAccountDescription') }}</p>
</div>
<div class="fields system">
<dl class="field" v-if="user.location">
<dt class="name"><Fa :icon="faMapMarker" fixed-width/> {{ $t('location') }}</dt>
<dd class="value">{{ user.location }}</dd>
</dl>
<dl class="field" v-if="user.birthday">
<dt class="name"><Fa :icon="faBirthdayCake" fixed-width/> {{ $t('birthday') }}</dt>
<dd class="value">{{ user.birthday.replace('-', '/').replace('-', '/') }} ({{ $t('yearsOld', { age }) }})</dd>
</dl>
<dl class="field">
<dt class="name"><Fa :icon="faCalendarAlt" fixed-width/> {{ $t('registeredDate') }}</dt>
<dd class="value">{{ new Date(user.createdAt).toLocaleString() }} (<MkTime :time="user.createdAt"/>)</dd>
</dl>
</div>
<div class="fields" v-if="user.fields.length > 0">
<dl class="field" v-for="(field, i) in user.fields" :key="i">
<dt class="name">
<Mfm :text="field.name" :plain="true" :custom-emojis="user.emojis" :colored="false"/>
</dt>
<dd class="value">
<Mfm :text="field.value" :author="user" :i="$store.state.i" :custom-emojis="user.emojis" :colored="false"/>
</dd>
</dl>
</div>
<div class="status">
<MkA :to="userPage(user)" :class="{ active: page === 'index' }">
<b>{{ number(user.notesCount) }}</b>
<span>{{ $t('notes') }}</span>
</MkA>
<MkA :to="userPage(user, 'following')" :class="{ active: page === 'following' }">
<b>{{ number(user.followingCount) }}</b>
<span>{{ $t('following') }}</span>
</MkA>
<MkA :to="userPage(user, 'followers')" :class="{ active: page === 'followers' }">
<b>{{ number(user.followersCount) }}</b>
<span>{{ $t('followers') }}</span>
</MkA>
</div>
<div :class="{ _section: narrow === true, _vMargin: narrow === false }">
<XUserTimeline :user="user" class="_content"/>
</div>
</template>
<XFollowList v-else-if="page === 'following'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="following" :user="user"/>
<XFollowList v-else-if="page === 'followers'" :class="{ _section: narrow === true, _vMargin: narrow === false }" type="followers" :user="user"/>
</div>
<div class="side" v-if="narrow === false">
<XPhotos class="_vMargin" :user="user" :key="user.id"/>
<XActivity class="_vMargin" :user="user" :key="user.id"/>
</div>
</div>
<template v-if="page === 'index'">
<div class="_section">
<div class="_content _vMargin" v-if="user.pinnedNotes.length > 0">
<XNote v-for="note in user.pinnedNotes" class="note _vMargin" :note="note" @update:note="pinnedNoteUpdated(note, $event)" :key="note.id" :detail="true" :pinned="true"/>
</div>
<MkFolder :body-togglable="true" class="_content _vMargin" persist-key="user-images">
<template #header><Fa :icon="faImage" style="margin-right: 0.5em;"/>{{ $t('images') }}</template>
<div>
<XPhotos :user="user" :key="user.id"/>
</div>
</MkFolder>
<MkFolder :body-togglable="true" class="_content _vMargin" persist-key="user-activity">
<template #header><Fa :icon="faChartBar" style="margin-right: 0.5em;"/>{{ $t('activity') }}</template>
<div>
<XActivity :user="user" :key="user.id"/>
</div>
</MkFolder>
</div>
<div class="_section">
<XUserTimeline :user="user" class="_content"/>
</div>
</template>
<XFollowList v-else-if="page === 'following'" type="following" :user="user"/>
<XFollowList v-else-if="page === 'followers'" type="followers" :user="user"/>
</div>
<div v-else-if="error">
<MkError @retry="fetch()"/>
<div v-else-if="error">
<MkError @retry="fetch()"/>
</div>
</div>
</template>
@@ -170,6 +168,7 @@ export default defineComponent({
user: null,
error: null,
parallaxAnimationId: null,
narrow: null,
faExclamationTriangle, faEllipsisH, faRobot, faLock, faBookmark, farBookmark, faChartBar, faImage, faBirthdayCake, faMapMarker, faCalendarAlt
};
},
@@ -197,6 +196,7 @@ export default defineComponent({
mounted() {
window.requestAnimationFrame(this.parallaxLoop);
this.narrow = this.$el.clientWidth < 1000;
},
beforeUnmount() {
@@ -254,220 +254,234 @@ export default defineComponent({
<style lang="scss" scoped>
.mk-user-page {
> .punished {
font-size: 0.8em;
padding: 16px;
}
display: flex;
max-width: 1050px;
margin: 0 auto;
> .main {
flex: 1;
> .profile {
> ._content {
position: relative;
overflow: hidden;
> .punished {
font-size: 0.8em;
padding: 16px;
}
> .banner-container {
> .profile {
> ._content {
position: relative;
height: 250px;
overflow: hidden;
background-size: cover;
background-position: center;
border-radius: 12px;
> .banner {
height: 100%;
background-color: #4c5e6d;
> .banner-container {
position: relative;
height: 250px;
overflow: hidden;
background-size: cover;
background-position: center;
box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
will-change: background-position;
}
> .fade {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 78px;
background: linear-gradient(transparent, rgba(#000, 0.7));
}
> .banner {
height: 100%;
background-color: #4c5e6d;
background-size: cover;
background-position: center;
box-shadow: 0 0 128px rgba(0, 0, 0, 0.5) inset;
will-change: background-position;
}
> .followed {
position: absolute;
top: 12px;
left: 12px;
padding: 4px 8px;
color: #fff;
background: rgba(0, 0, 0, 0.7);
font-size: 0.7em;
border-radius: 6px;
}
> .fade {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 78px;
background: linear-gradient(transparent, rgba(#000, 0.7));
}
> .actions {
position: absolute;
top: 12px;
right: 12px;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.2);
padding: 8px;
border-radius: 24px;
> .menu {
vertical-align: bottom;
height: 31px;
width: 31px;
> .followed {
position: absolute;
top: 12px;
left: 12px;
padding: 4px 8px;
color: #fff;
text-shadow: 0 0 8px #000;
font-size: 16px;
background: rgba(0, 0, 0, 0.7);
font-size: 0.7em;
border-radius: 6px;
}
> .koudoku {
margin-left: 4px;
vertical-align: bottom;
}
}
> .actions {
position: absolute;
top: 12px;
right: 12px;
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background: rgba(0, 0, 0, 0.2);
padding: 8px;
border-radius: 24px;
> .title {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 0 0 8px 154px;
box-sizing: border-box;
color: #fff;
> .menu {
vertical-align: bottom;
height: 31px;
width: 31px;
color: #fff;
text-shadow: 0 0 8px #000;
font-size: 16px;
}
> .name {
display: block;
margin: 0;
line-height: 32px;
font-weight: bold;
font-size: 1.8em;
text-shadow: 0 0 8px #000;
> .koudoku {
margin-left: 4px;
vertical-align: bottom;
}
}
> .bottom {
> * {
display: inline-block;
margin-right: 16px;
line-height: 20px;
opacity: 0.8;
> .title {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 0 0 8px 154px;
box-sizing: border-box;
color: #fff;
&.username {
font-weight: bold;
> .name {
display: block;
margin: 0;
line-height: 32px;
font-weight: bold;
font-size: 1.8em;
text-shadow: 0 0 8px #000;
}
> .bottom {
> * {
display: inline-block;
margin-right: 16px;
line-height: 20px;
opacity: 0.8;
&.username {
font-weight: bold;
}
}
}
}
}
}
> .title {
display: none;
text-align: center;
padding: 50px 8px 16px 8px;
font-weight: bold;
border-bottom: solid 1px var(--divider);
> .bottom {
> * {
display: inline-block;
margin-right: 8px;
opacity: 0.8;
}
}
}
> .avatar {
display: block;
position: absolute;
top: 170px;
left: 16px;
z-index: 2;
width: 120px;
height: 120px;
box-shadow: 1px 1px 3px rgba(#000, 0.2);
}
> .description {
padding: 24px 24px 24px 154px;
font-size: 0.95em;
> .empty {
margin: 0;
opacity: 0.5;
}
}
> .fields {
padding: 24px;
font-size: 0.9em;
border-top: solid 1px var(--divider);
> .field {
display: flex;
padding: 0;
margin: 0;
align-items: center;
&:not(:last-child) {
margin-bottom: 8px;
}
> .name {
width: 30%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold;
text-align: center;
}
> .value {
width: 70%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
&.system > .field > .name {
}
}
> .status {
display: flex;
padding: 24px;
border-top: solid 1px var(--divider);
> a {
flex: 1;
> .title {
display: none;
text-align: center;
padding: 50px 8px 16px 8px;
font-weight: bold;
border-bottom: solid 1px var(--divider);
&.active {
color: var(--accent);
> .bottom {
> * {
display: inline-block;
margin-right: 8px;
opacity: 0.8;
}
}
}
> .avatar {
display: block;
position: absolute;
top: 170px;
left: 16px;
z-index: 2;
width: 120px;
height: 120px;
box-shadow: 1px 1px 3px rgba(#000, 0.2);
}
> .description {
padding: 24px 24px 24px 154px;
font-size: 0.95em;
> .empty {
margin: 0;
opacity: 0.5;
}
}
> .fields {
padding: 24px;
font-size: 0.9em;
border-top: solid 1px var(--divider);
> .field {
display: flex;
padding: 0;
margin: 0;
align-items: center;
&:not(:last-child) {
margin-bottom: 8px;
}
> .name {
width: 30%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold;
text-align: center;
}
> .value {
width: 70%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
&:hover {
text-decoration: none;
&.system > .field > .name {
}
}
> b {
display: block;
line-height: 16px;
}
> .status {
display: flex;
padding: 24px;
border-top: solid 1px var(--divider);
> span {
font-size: 70%;
> a {
flex: 1;
text-align: center;
&.active {
color: var(--accent);
}
&:hover {
text-decoration: none;
}
> b {
display: block;
line-height: 16px;
}
> span {
font-size: 70%;
}
}
}
}
}
> .content {
margin-bottom: var(--margin);
}
}
> .content {
margin-bottom: var(--margin);
> .side {
flex-basis: 300px;
margin-left: var(--margin);
}
&.max-width_500px {
> .profile > ._content {
display: block;
> .main > .profile > ._content {
> .banner-container {
height: 140px;

View File

@@ -0,0 +1,141 @@
<template>
<div class="xyeqzsjl _panel">
<header>
<button class="_button" @click="back()" v-if="history.length > 0"><Fa :icon="faChevronLeft"/></button>
<XHeader class="title" :info="pageInfo" :with-back="false"/>
</header>
<div>
<component :is="component" v-bind="props" :ref="changePage"/>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons';
import XWindow from '@/components/ui/window.vue';
import XHeader from '@/ui/_common_/header.vue';
import { popout } from '@/scripts/popout';
import { resolve } from '@/router';
import { url } from '@/config';
export default defineComponent({
components: {
XWindow,
XHeader,
},
provide() {
return {
navHook: (path) => {
this.navigate(path);
}
};
},
props: {
initialPath: {
type: String,
required: true,
},
},
data() {
return {
pageInfo: null,
path: this.initialPath,
component: null,
props: null,
history: [],
faChevronLeft,
};
},
computed: {
url(): string {
return url + this.path;
},
},
created() {
const { component, props } = resolve(this.initialPath);
this.component = component;
this.props = props;
},
methods: {
changePage(page) {
if (page == null) return;
if (page.INFO) {
this.pageInfo = page.INFO;
}
},
navigate(path, record = true) {
if (record) this.history.push(this.path);
this.path = path;
const { component, props } = resolve(path);
this.component = component;
this.props = props;
},
back() {
this.navigate(this.history.pop(), false);
},
expand() {
this.$router.push(this.path);
this.$refs.window.close();
},
popout() {
popout(this.path, this.$el);
this.$refs.window.close();
},
},
});
</script>
<style lang="scss" scoped>
.xyeqzsjl {
--section-padding: 16px;
display: flex;
flex-direction: column;
contain: content;
> header {
$height: 50px;
display: flex;
position: relative;
z-index: 1;
height: $height;
line-height: $height;
box-shadow: 0px 1px var(--divider);
> button {
height: $height;
width: $height;
&:hover {
color: var(--fgHighlighted);
}
}
> .title {
flex: 1;
position: relative;
line-height: $height;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
}
> div {
flex: 1;
overflow: auto;
}
}
</style>

View File

@@ -1,18 +1,7 @@
<template>
<div class="rsqzvsbo">
<div class="_section">
<div class="_content _panel about" v-if="meta">
<div class="body">
<div class="desc" v-html="meta.description || $t('introMisskey')"></div>
<MkButton @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</MkButton>
<MkButton @click="signin()" style="display: inline-block;">{{ $t('login') }}</MkButton>
</div>
</div>
</div>
<div class="_section">
<div class="_content">
<XNotes :pagination="featuredPagination"/>
</div>
<div class="rsqzvsbo _section" v-if="meta">
<div class="blocks">
<XBlock class="block" v-for="path in meta.pinnedPages" :initial-path="path" :key="path"/>
</div>
</div>
</template>
@@ -24,33 +13,30 @@ import XSigninDialog from '@/components/signin-dialog.vue';
import XSignupDialog from '@/components/signup-dialog.vue';
import MkButton from '@/components/ui/button.vue';
import XNotes from '@/components/notes.vue';
import { host } from '@/config';
import XBlock from './welcome.entrance.block.vue';
import { host, instanceName } from '@/config';
import * as os from '@/os';
export default defineComponent({
components: {
MkButton,
XNotes,
XBlock,
},
data() {
return {
featuredPagination: {
endpoint: 'notes/featured',
limit: 10,
noPaging: true,
},
host: toUnicode(host),
instanceName,
meta: null,
};
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
created() {
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
os.api('stats').then(stats => {
this.stats = stats;
});
@@ -74,15 +60,20 @@ export default defineComponent({
<style lang="scss" scoped>
.rsqzvsbo {
> ._section {
> .about {
> .body {
padding: 32px;
text-align: center;
@media (max-width: 500px) {
padding: 16px;
}
}
> .blocks {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
grid-gap: var(--margin);
text-align: left;
> .block {
height: 600px;
}
@media (max-width: 800px) {
grid-template-columns: 1fr;
}
}
}

View File

@@ -10,6 +10,7 @@ import { defineComponent } from 'vue';
import XSetup from './welcome.setup.vue';
import XEntrance from './welcome.entrance.vue';
import { instanceName } from '@/config';
import * as os from '@/os';
export default defineComponent({
components: {
@@ -20,16 +21,17 @@ export default defineComponent({
data() {
return {
INFO: {
title: instanceName || 'Misskey',
title: instanceName,
icon: null
},
meta: null
}
},
computed: {
meta() {
return this.$store.state.instance.meta;
},
},
created() {
os.api('meta', { detail: true }).then(meta => {
this.meta = meta;
});
}
});
</script>

View File

@@ -22,7 +22,7 @@ export const router = createRouter({
{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
{ path: '/@:acct/room', props: true, component: page('room/room') },
{ path: '/settings/:page?', name: 'settings', component: page('settings/index'), props: route => ({ page: route.params.page || null }) },
{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ page: route.params.page || null }) },
{ path: '/announcements', component: page('announcements') },
{ path: '/about', component: page('about') },
{ path: '/about-misskey', component: page('about-misskey') },
@@ -33,6 +33,9 @@ export const router = createRouter({
{ path: '/explore', component: page('explore') },
{ path: '/explore/tags/:tag', props: true, component: page('explore') },
{ path: '/search', component: page('search') },
{ path: '/pages', name: 'pages', component: page('pages') },
{ path: '/pages/new', component: page('page-editor/page-editor') },
{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
{ path: '/channels', component: page('channels') },
{ path: '/channels/new', component: page('channel-editor') },
{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true },
@@ -47,9 +50,6 @@ export const router = createRouter({
{ path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) },
{ path: '/my/drive', name: 'drive', component: page('drive') },
{ path: '/my/drive/folder/:folder', component: page('drive') },
{ path: '/my/pages', name: 'pages', component: page('pages') },
{ path: '/my/pages/new', component: page('page-editor/page-editor') },
{ path: '/my/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
{ path: '/my/follow-requests', component: page('follow-requests') },
{ path: '/my/lists', component: page('my-lists/index') },
{ path: '/my/lists/:list', component: page('my-lists/list') },
@@ -57,7 +57,6 @@ export const router = createRouter({
{ path: '/my/groups/:group', component: page('my-groups/group') },
{ path: '/my/antennas', component: page('my-antennas/index') },
{ path: '/my/clips', component: page('my-clips/index') },
{ path: '/my/apps', component: page('apps') },
{ path: '/scratchpad', component: page('scratchpad') },
{ path: '/instance', component: page('instance/index') },
{ path: '/instance/emojis', component: page('instance/emojis') },

View File

@@ -0,0 +1,24 @@
import { device } from '@/cold-storage';
const cache = new Map<string, HTMLAudioElement>();
export function play(type: string) {
const sound = device.get('sound_' + type as any);
if (sound.type == null) return;
playFile(sound.type, sound.volume);
}
export function playFile(file: string, volume: number) {
const masterVolume = device.get('sound_masterVolume');
if (masterVolume === 0) return;
let audio: HTMLAudioElement;
if (cache.has(file)) {
audio = cache.get(file);
} else {
audio = new Audio(`/assets/sounds/${file}.mp3`);
cache.set(file, audio);
}
audio.volume = masterVolume - ((1 - volume) * masterVolume);
audio.play();
}

View File

@@ -15,19 +15,12 @@ export const darkTheme: Theme = require('../themes/_dark.json5');
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
export const builtinThemes = [
require('../themes/l-white.json5'),
require('../themes/l-red.json5'),
require('../themes/l-green.json5'),
require('../themes/l-blue.json5'),
require('../themes/l-light.json5'),
require('../themes/l-apricot.json5'),
require('../themes/d-black.json5'),
require('../themes/d-red.json5'),
require('../themes/d-green.json5'),
require('../themes/d-blue.json5'),
require('../themes/d-dark.json5'),
require('../themes/d-persimmon.json5'),
require('../themes/d-battery-saver.json5'),
require('../themes/d-black.json5'),
] as Theme[];
let timeout = null;

View File

@@ -96,8 +96,7 @@ export const sidebarDef = {
pages: {
title: 'pages',
icon: faFileAlt,
show: computed(() => store.getters.isSignedIn),
to: '/my/pages',
to: '/pages',
},
clips: {
title: 'clip',

View File

@@ -55,7 +55,7 @@ export const defaultDeviceUserSettings = {
export const defaultDeviceSettings = {
lang: null,
loadRawImages: false,
alwaysShowNsfw: false,
nsfw: 'respect', // respect, force, ignore
useOsNativeEmojis: false,
serverDisconnectedBehavior: 'quiet',
accounts: [],
@@ -78,6 +78,8 @@ export const defaultDeviceSettings = {
enableInfiniteScroll: true,
useBlurEffectForModal: true,
useFullReactionPicker: false,
reactionPickerWidth: 1,
reactionPickerHeight: 1,
sidebarDisplay: 'full', // full, icon, hide
instanceTicker: 'remote', // none, remote, always
roomGraphicsQuality: 'medium',
@@ -85,14 +87,6 @@ export const defaultDeviceSettings = {
deckColumnAlign: 'left',
deckAlwaysShowMainColumn: true,
deckMainColumnPlace: 'left',
sfxVolume: 0.3,
sfxNote: 'syuilo/down',
sfxNoteMy: 'syuilo/up',
sfxNotification: 'syuilo/pope2',
sfxChat: 'syuilo/pope1',
sfxChatBg: 'syuilo/waon',
sfxAntenna: 'syuilo/triple',
sfxChannel: 'syuilo/square-pico',
userData: {},
};

View File

@@ -26,6 +26,9 @@ html {
background-position: center;
color: var(--fg);
overflow: auto;
font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
line-height: 1.3em;
text-size-adjust: 100%;
&, * {
scrollbar-color: var(--scrollbarHandle) var(--panel);
@@ -88,8 +91,6 @@ html, body {
margin: 0;
padding: 0;
scroll-behavior: smooth;
text-size-adjust: 100%;
font-family: Roboto, HelveticaNeue, Arial, sans-serif;
}
a {
@@ -160,6 +161,8 @@ hr {
tap-highlight-color: transparent;
-webkit-tap-highlight-color: transparent;
font-size: 1em;
font-family: inherit;
line-height: inherit;
&, * {
@extend ._noSelect;
@@ -445,10 +448,14 @@ hr {
opacity: 0.7;
}
._monospace {
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
}
._code {
@extend ._monospace;
background: #2d2d2d;
color: #ccc;
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
font-size: 14px;
line-height: 1.5;
padding: 5px;

View File

@@ -19,6 +19,7 @@
divider: 'rgba(255, 255, 255, 0.1)',
indicator: '@accent',
panel: '#000',
panelHighlight: ':lighten<3<@panel',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -19,6 +19,7 @@
divider: 'rgba(0, 0, 0, 0.1)',
indicator: '@accent',
panel: '#fff',
panelHighlight: ':darken<3<@panel',
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',

View File

@@ -1,18 +0,0 @@
{
id: '8c539dc1-0fab-4d47-9194-39c508e9bfe1',
name: 'Battery Saver',
author: 'syuilo',
base: 'dark',
props: {
divider: '#2d2d2d',
panelHeaderBg: '@panel',
panelHeaderDivider: '@divider',
panelShadow: '" 0 0 0 1px var(--divider)',
shadow: 'rgba(255, 255, 255, 0.05)',
modalBg: 'rgba(255, 255, 255, 0.1)',
messageBg: '#1d1d1d',
},
}

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