1
0
Fork 1
mirror of https://example.com synced 2024-11-22 12:56:39 +09:00

Firefish v1.0.4-beta31

This commit is contained in:
naskya 2023-09-21 07:53:48 +09:00
parent a93f971703
commit 63b9905a71
Signed by: naskya
GPG key ID: 164DFF24E2D40139
56 changed files with 1621 additions and 1724 deletions

View file

@ -474,3 +474,4 @@ _preferencesBackups:
cannotLoad: Неуспешно зареждане
editWidgetsExit: Готово
done: Готово
emailRequiredForSignup: Изискване за адрес на е-поща за регистриране

View file

@ -2190,3 +2190,5 @@ detectPostLanguage: Detecta l'idioma automàticament i mostra un botó per els a
indexableDescription: Permet al cercador intern mostrar els missatges públics
indexable: Indexable
languageForTranslation: Idioma de traducció d'articles
openServerInfo: Mostra la informació del servidor fent clic al símbol del servidor
en un missatge

View file

@ -2103,7 +2103,7 @@ pwa: PWA installieren
cw: Inhaltswarnung
older: älter
newer: neuer
accessibility: Erreichbarkeit
accessibility: Barrierefreiheit
jumpToPrevious: Zum Vorherigen springen
silencedWarning: Diese Meldung wird angezeigt, weil diese Nutzer von Servern stammen,
die Ihr Administrator abgeschaltet hat, so dass es sich möglicherweise um Spam handelt.
@ -2213,3 +2213,5 @@ indexableDescription: Der integrierten Suche erlauben, Ihre öffentlichen Beitr
anzuzeigen
indexable: Indexierbar
languageForTranslation: Übersetzungssprache veröffentlichen
openServerInfo: Anzeigen von Serverinformationen durch Anklicken des Server-Tickers
in einem Beitrag

View file

@ -675,7 +675,7 @@ emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP verif
smtpSecure: "Use implicit SSL/TLS for SMTP connections"
smtpSecureInfo: "Turn this off when using STARTTLS"
testEmail: "Test email delivery"
wordMute: "Word mute"
wordMute: "Word and language mutes"
regexpError: "Regular Expression error"
regexpErrorDescription: "An error occurred in the regular expression on line {line}
of your {tab} word mutes:"
@ -1151,6 +1151,8 @@ indexable: "Indexable"
indexableDescription: "Allow built-in search to show your public posts"
languageForTranslation: "Post translation language"
detectPostLanguage: "Automatically detect the language and show a translate button for posts in foreign languages"
vibrate: "Play vibrations"
openServerInfo: "Show server information by clicking the server ticker on a post"
_sensitiveMediaDetection:
description: "Reduces the effort of server moderation through automatically recognizing
@ -1382,14 +1384,19 @@ _menuDisplay:
hide: "Hide"
_wordMute:
muteWords: "Muted words"
muteLangs: "Muted Languages"
muteWordsDescription: "Separate with spaces for an AND condition or with line breaks
for an OR condition."
muteWordsDescription2: "Surround keywords with slashes to use regular expressions."
muteLangsDescription: "Separate with spaces or line breaks for an OR condition."
muteLangsDescription2: "Use language code e.g. en, fr, ja, zh."
softDescription: "Hide posts that fulfil the set conditions from the timeline."
langDescription: "Hide posts that match set language from the timeline."
hardDescription: "Prevents posts fulfilling the set conditions from being added
to the timeline. In addition, these posts will not be added to the timeline even
if the conditions are changed."
soft: "Soft"
lang: "Language"
hard: "Hard"
mutedNotes: "Muted posts"
_instanceMute:

View file

@ -724,8 +724,8 @@ no: "Non"
driveFilesCount: "Nombre de fichiers dans le Drive"
driveUsage: "Utilisation du Drive"
noCrawle: "Refuser l'indexation par les robots"
noCrawleDescription: "Demandez aux moteurs de recherche de ne pas indexer votre page
de profil, vos publications, vos pages, etc."
noCrawleDescription: "Demandez aux moteurs de recherche externes de ne pas indexer
votre contenu."
lockedAccountInfo: "À moins que vous ne définissiez la visibilité de votre publication
sur \"Abonné-e-s\", vos publications sont visibles par tous, même si vous exigez
que les demandes d'abonnement soient approuvées manuellement."
@ -2210,3 +2210,11 @@ confirm: Confirmer
importZip: Importer ZIP
exportZip: Exporter ZIP
emojiPackCreator: Créateur de pack démoji
detectPostLanguage: Détecte automatiquement le langage et affiche un bouton de traduction
pour les langues étrangères
indexableDescription: Permettre à la recherche interne dafficher vos publications
publiques
openServerInfo: Afficher les informations du serveur en cliquant sur le bandeau de
serveur dune publication
indexable: Indexable
languageForTranslation: Langage post-traduction

View file

@ -81,7 +81,7 @@ deleteAndEdit: Eliminar e editar
blockConfirm: Tes a certeza de querer bloquear esta conta?
deleteAndEditConfirm: Tes a certeza de querer eliminar esta publicación e editala?
Perderás todas as súas reaccións, promocións e respostas.
editNote: Editar nota
editNote: Editar publicación
edited: Editado o {date} {time}
sendMessage: Enviar unha mensaxe
copyUsername: Copiar identificador
@ -282,3 +282,76 @@ storageUsage: Uso da almacenaxe
charts: Gráficas
perHour: Por hora
followRequest: Solicitar seguimento
messageRead: Ler
noMoreHistory: Non hai máis historial
images: Imaxes
manageGroups: Xestionar grupos
unableToDelete: Non se puido eliminar
syncDeviceDarkMode: Syncr Modo Escuro cos axustes do teu dispositivo
uploadFromUrl: Subir desde un URL
emptyDrive: O teu Disco está baleiro
copyUrl: Copiar URL
nUsersRead: lido por {n}
uploadFromUrlRequested: Solicitaches unha subida
circularReferenceFolder: O cartafol de destino é un subcartafol do cartafol que queres
mover.
selectFile: Elexir un ficheiro
inputNewFileName: Escribe o novo nome
agreeTo: Acepto os {0}
whenServerDisconnected: Cando se perda a conexión co servidor
selectFolder: Elexir un cartafol
saved: Gardado
selectFiles: Elexir ficheiros
fileName: Nome do ficheiro
explore: Descubrir
keepOriginalUploadingDescription: Garda a imaxe subida orixinalmente tal como é. Se
o desactivas, a versión que se mostrará na web será creada ao subila.
folderName: Nome do cartafol
lightThemes: Decorados claros
rename: Cambiar nome
activity: Actividade
fromUrl: Desde URL
darkThemes: Decorados escuros
birthday: Aniversario
registeredDate: Conta creada en
fromDrive: Desde Disco
uploadFromUrlMayTakeTime: Podería tardar una anaco en completarse a subida.
theme: Decorados
renameFolder: Cambiar nome a este cartafol
resetAreYouSure: Queres restablecer?
startMessaging: Comezar un novo chat
light: Claro
themeForLightMode: Decorado a usar no Modo Claro
inputNewDescription: Escribe a descrición
start: Comezar
selectFolders: Elexir cartafoles
remoteUserCaution: A información das usuarias remotas podería estar incompleta.
exportRequested: Solicitaches unha exportación. Vainos levar un pouco. Vai ser engadida
ao teu Disco cando estea completa.
deleteFolder: Eliminar este cartafol
drive: Disco
importRequested: Solicitaches unha importación. Vainos levar un anaco.
uploadFromUrlDescription: URL do ficheiro que queres subir
location: Localización
unfollowConfirm: Tes a certeza de que queres deixar de seguir a {name}?
banner: Cabeceira
dark: Escuro
home: Inicio
keepOriginalUploading: Manter imaxe orixinal
upload: Subir
yearsOld: '{age} anos de idade'
emptyFolder: Este cartafol está baleiro
messaging: Chat
nsfw: NSFW
addFile: Engadir un ficheiro
tos: Termos do Servizo
themeForDarkMode: Decorado a usar no Modo Escuro
deleteAreYouSure: Tes a certeza de querer desbotar "{x}"?
createFolder: Crear un cartafol
renameFile: Cambiar nome ao ficheiro
lookup: Buscar
avatar: Avatar
driveFileDeleteConfirm: Tes a certeza de querer eliminar o ficheiro "{name}"? Vai
ser retirado de todas as publicacións nas que estea como anexo.
inputNewFolderName: Escribe o novo nome do cartafol
hasChildFilesOrFolders: Como o cartafol non está baleiro, non pode ser eliminado.

View file

@ -245,7 +245,7 @@ currentPassword: "Kata sandi saat ini"
newPassword: "Kata sandi baru"
newPasswordRetype: "Ulangi kata sandi baru"
attachFile: "Lampirkan berkas"
more: "Lagi !"
more: "Lagi!"
featured: "Sorotan"
usernameOrUserId: "Nama pengguna atau User ID"
noSuchUser: "Pengguna tidak ditemukan"
@ -2173,3 +2173,5 @@ detectPostLanguage: Deteksi bahasa secara otomatis dan tampilkan tombol terjemah
indexableDescription: Perbolehkan pencarian di sini untuk menampilkan kiriman publikmu
indexable: Dapat diindeks
languageForTranslation: Bahasa terjemahan kiriman
openServerInfo: Tampilkan informasi server dengan mengeklik ticker server di sebuah
kiriman

View file

@ -2162,3 +2162,5 @@ detectPostLanguage: Riconosci la lingua automaticamente e mostra il bottone per
indexableDescription: Mostra i tuoi post pubblici tramite il sistema di ricerca
indexable: Indicizzabile
languageForTranslation: Linguaggio di traduzione dei post
openServerInfo: Mostra informazioni sul server cliccando sul riquadro del server in
un post

View file

@ -66,7 +66,7 @@ import: "インポート"
export: "エクスポート"
files: "ファイル"
download: "ダウンロード"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?これにより、このファイルが添付されている投稿も削除されます。"
driveFileDeleteConfirm: "ファイル「{name}」を削除しますか?これにより、添付ファイルとして含まれているすべての投稿から削除されます。"
unfollowConfirm: "{name}さんのフォローを解除しますか?"
exportRequested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、「ドライブ」に追加されます。"
importRequested: "インポートをリクエストしました。これには時間がかかる場合があります。"
@ -1209,11 +1209,16 @@ _menuDisplay:
hide: "隠す"
_wordMute:
muteWords: "ミュートするワード"
muteLangs: "ミュートされた言語"
muteWordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。"
muteWordsDescription2: "キーワードをスラッシュで囲むと正規表現になります。"
muteLangsDescription: "OR 条件の場合はスペースまたは改行で区切ります。"
muteLangsDescription2: "言語コードを使用します。例: en, fr, ja, zh."
softDescription: "指定した条件の投稿をタイムラインから隠します。"
langDescription: "設定した言語に一致する投稿をタイムラインから非表示にします。"
hardDescription: "指定した条件の投稿をタイムラインに追加しないようにします。追加されなかった投稿は、条件を変更しても除外されたままになります。"
soft: "ソフト"
lang: "言語"
hard: "ハード"
mutedNotes: "ミュートされた投稿"
_instanceMute:
@ -1982,7 +1987,7 @@ _feeds:
rss: RSS
jsonFeed: JSONフィード
copyFeed: フィードのURLをコピー
origin: オリジナル
origin: 元のサーバー
delete2fa: 2要素認証を無効化
deletePasskeys: パスキーを削除
delete2faConfirm: これで、このアカウントの2要素認証は完全に削除されます。続行しますか?
@ -2003,3 +2008,4 @@ privacyForNerds: "身バレ防止"
disableToast: "「おかえりなさい、◯◯さん」を表示しない"
hideMyIcon: "自分のアイコンを表示しない"
hideMyName: "自分の名前とIDを表示しない"
openServerInfo: "投稿内のサーバー名をクリックでサーバー情報を開く"

View file

@ -578,7 +578,7 @@ disableAll: "全部使えへんようにする"
tokenRequested: "アカウントへのアクセス許可"
pluginTokenRequestedDescription: "このプラグインはここで設定した権限を使えるようになるで。"
notificationType: "通知の種類"
edit: "編集"
edit: "投稿をいじる"
emailServer: "メールサーバー"
enableEmail: "メール配信を受け取る"
emailConfigInfo: "メールアドレスの確認とかパスワードリセットの時に使うで"
@ -735,7 +735,7 @@ showingPastTimeline: "過去のタイムラインを表示してるで"
clear: "クリア"
markAllAsRead: "もうみな読んでもうたわ"
goBack: "戻る"
unlikeConfirm: "いいね解除するんか"
unlikeConfirm: "ええやんを解除するんけ"
fullView: "フルビュー"
quitFullView: "フルビュー解除"
addDescription: "説明を追加するで"
@ -810,7 +810,7 @@ searchByGoogle: "探す"
indefinitely: "無期限"
file: "ファイル"
requireAdminForView: "これを見るには管理者アカウントでログインしとらなあかんで。"
isSystemAccount: "システムが自動で作成・管理しとるアカウントやで。"
isSystemAccount: "システムが自動で作成・管理しとるアカウントやで。モデレーション・編集・削除するとサーバーの動作が不正になる可能性があるので、操作せんといてください。"
typeToConfirm: "この操作をやるんなら {x} と入力してなー"
deleteAccount: "アカウント削除するで"
document: "ドキュメント"
@ -853,7 +853,9 @@ _ffVisibility:
_ad:
back: "戻る"
_gallery:
unlike: "良くないわ"
unlike: "やっぱよくないわ"
like: ええやん!
liked: ええやんと思った投稿
_email:
_follow:
title: "フォローされたで"
@ -1069,9 +1071,16 @@ _poll:
votesCount: "{n}票"
vote: "投票する"
_visibility:
publicDescription: "みんなに公開"
home: "ホーム"
followers: "フォロワー"
publicDescription: "うちの投稿、みんな見てや"
home: "ホームタイムラインのみ"
followers: "フォロワーのみ"
localOnly: ローカルのみ
followersDescription: フォロワーと返信相手だけに見せたる
specified: ダイレクト
localOnlyDescription: 他のサーバーには見せとうない
specifiedDescription: 指定した相手だけに見せたる
public: 公開
homeDescription: ローカルTLやグローバルTLには流さへん
_profile:
name: "名前"
username: "ユーザー名"
@ -1121,7 +1130,7 @@ _pages:
pageSetting: "ページ設定"
viewPage: "ページを見る"
like: "ええやん"
unlike: "良くないわ"
unlike: "やっぱ気に入らん"
liked: "ええと思ったページ"
contents: "コンテンツ"
summary: "ページの要約"
@ -1441,5 +1450,6 @@ _postForm:
f: あんさん書くんを待っとるんどす...
a: いまなにしとん?
flagSpeakAsCat: 猫弁で話す
flagSpeakAsCatDescription: 猫モードが有効の場合にオンにすると、ワレの投稿の「な」を「にゃ」に変換るで。
flagSpeakAsCatDescription: オンにすると、ワレの投稿の「な」を「にゃ」に変換したるで。
welcomeBackWithName: おおきに、{name}はん
migration: アカウントの引っ越し

View file

@ -300,7 +300,7 @@ messagingWithGroup: Grup sohbeti
next: Sonraki
retype: Tekrar gir
dashboard: Panel
objectStorageBucket: Bucket
objectStorageBucket: Kova
objectStorageBucketDesc: Sağlayıcınız tarafından kullanınan bucket ismini yazın.
showFixedPostForm: Gönderim formunu zaman çizelgesinin en üstünde görüntüleyin
newNoteRecived: Yeni gönderiler mevcut
@ -750,7 +750,7 @@ upload: Yükle
fromUrl: URL'den
agreeTo: '{0} kabul ediyorum'
tos: Kullanım Koşulları
drive: Drive
drive: Sürücü
selectFolder: Klasör seç
inputNewFileName: Yeni dosya ismi gir
whenServerDisconnected: Sunucuyla bağlantı kesildiğinde
@ -916,7 +916,7 @@ aboutX: '{x} Hakkında'
doing: İşleniyor...
category: Kategori
deleteAll: Hepsini sil
objectStorageEndpoint: Endpoint
objectStorageEndpoint: Uç noktası
output: Çıkış
userSuspended: Bu kullanıcı askıya alındı.
userSilenced: Bu kullanıcı susturuldu.
@ -1105,7 +1105,7 @@ clips: Ataçlar
experimentalFeatures: Deneysel özellikler
developer: Geliştirici
left: Sol
center: Orta
center: Merkez
wide: Geniş
narrow: Dar
reloadToApplySetting: Bu ayar yalnızca bir sayfa yeniden yüklendikten sonra geçerli

View file

@ -1111,11 +1111,16 @@ _menuDisplay:
hide: "隐藏"
_wordMute:
muteWords: "过滤词"
muteLangs: "过滤语言"
muteWordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。"
muteWordsDescription2: "将关键字用斜线括起来表示正则表达式。"
muteLangsDescription: "OR 条件用空格,换行符分隔"
muteLangsDescription2: "使用语言代码。例: en, fr, ja, zh."
softDescription: "隐藏时间线中指定条件的帖子。"
langDescription: "从时间线中隐藏与设置语言匹配的帖子。"
hardDescription: "防止将具有指定条件的帖子添加到时间线。 即使您更改条件,原先未添加的帖文也会被排除在外。"
soft: "软过滤"
lang: "语言"
hard: "硬过滤"
mutedNotes: "已过滤的帖子"
_instanceMute:

View file

@ -236,7 +236,7 @@ imageUrl: "圖片URL"
remove: "刪除"
removed: "已成功刪除"
removeAreYouSure: "確定要刪掉「{x}」嗎?"
deleteAreYouSure: "確定要刪「{x}」嗎?"
deleteAreYouSure: "確定要刪「{x}」嗎?"
resetAreYouSure: "確定要重設嗎?"
saved: "已儲存"
messaging: "訊息"
@ -292,7 +292,7 @@ inputNewFileName: "輸入檔案名稱"
inputNewDescription: "請輸入新標題"
inputNewFolderName: "輸入新資料夾的名稱"
circularReferenceFolder: "目標文件夾是您要移動的文件夾的子文件夾。"
hasChildFilesOrFolders: "此文件夾不是空的,無法刪除。"
hasChildFilesOrFolders: "此資料夾不是空的,無法刪除。"
copyUrl: "複製網址"
rename: "重新命名"
avatar: "大頭貼"
@ -525,14 +525,14 @@ state: "狀態"
sort: "排序"
ascendingOrder: "昇冪"
descendingOrder: "降冪"
scratchpad: "暫存記憶體"
scratchpad: "AiScript控制台"
scratchpadDescription: "AiScript控制台為AiScript提供了實驗環境。您可以在此編寫、執行和確認代碼與Firefish互動的结果。"
output: "輸出"
script: "腳本"
disablePagesScript: "停用頁面的AiScript腳本"
updateRemoteUser: "更新遠端使用者資訊"
deleteAllFiles: "刪除所有檔案"
deleteAllFilesConfirm: "要删除所有檔案嗎?"
deleteAllFilesConfirm: "確定要刪除所有檔案嗎?"
removeAllFollowing: "解除所有追蹤"
removeAllFollowingDescription: "解除{host}所有的追蹤。在伺服器不再存在時執行。"
userSuspended: "此使用者已被停用。"
@ -773,7 +773,7 @@ gallery: "相簿"
recentPosts: "最新貼文"
popularPosts: "熱門的貼文"
shareWithNote: "在貼文中分享"
ads: "廣告"
ads: "社群橫幅"
expiration: "期限"
memo: "備忘錄"
priority: "優先級"
@ -895,11 +895,11 @@ navbar: "導覽列"
shuffle: "隨機"
account: "帳戶"
move: "移動"
customKaTeXMacro: "自定義 KaTeX 宏"
customKaTeXMacroDescription: "使用宏來輕鬆的輸入數學表達式吧!宏的用法與 LaTeX 中的命令定義相同。你可以使用 \\newcommand{\\
name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 來輸入數學表達式。舉個例子\\
newcommand{\\add}[2]{#1 + #2} 會將 \\add{3}{foo} 展開為 3 + foo。此外,宏名稱外的花括號 {} 可以被替換為圓括號
() 和方括號 [],這會影響用於參數的括號。每行只能夠定義一個宏,無法在中間換行,且無效的行將被忽略。只支持簡單字符串替換功能,不支持高級語法,如條件分支等。"
customKaTeXMacro: "自訂KaTeX巨集"
customKaTeXMacroDescription: "使用巨集來輕鬆輸入數學表達式吧!巨集的用法與 LaTeX 中的命令定義相同。你可以使用 \\newcommand{\\
name}{content} 或 \\newcommand{\\name}[number of arguments]{content} 來輸入數學表達式。舉例來說\\
newcommand{\\add}[2]{#1 + #2} 會將 \\add{3}{foo} 展開為 3 + foo。巨集名稱除了可用大括號 {} 括起來之外,也可使用小括號
() 和中括號 [],但使用於巨集參數的括號會有所變更。每行只能夠定義一個巨集,巨集中間無法間換。無效的行將被忽略。只支援簡單字串的替換功能,不支援條件分歧的高級語法。"
enableCustomKaTeXMacro: "啟用自定義 KaTeX 宏"
_sensitiveMediaDetection:
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
@ -991,6 +991,7 @@ _aboutFirefish:
pleaseDonateToFirefish: 請考慮向 Firefish 贊助以支持其發展。
pleaseDonateToHost: 還請考慮捐贈給您在使用的伺服器 {host},以支援龐大的運營成本。
donateHost: 贊助給 {host}
misskeyContributors: Misskey的貢獻者
_nsfw:
respect: "隱藏敏感內容"
ignore: "不隱藏敏感內容"
@ -1259,6 +1260,8 @@ _2fa:
token: 兩步驟驗證金鑰
registerTOTPBeforeKey: 請設置身份驗證器應用程式以註冊安全金鑰或密碼。
renewTOTPOk: 重新配置
step3Title: 輸入驗證碼
securityKeyNotSupported: 您使用的瀏覧器不支援安全金鑰(Security key)。
_permissions:
"read:account": "查看我的帳戶資訊"
"write:account": "更改我的帳戶資訊"
@ -1803,6 +1806,7 @@ _deck:
list: "清單"
mentions: "提及"
direct: "指定使用者"
channel: 頻道
secureMode: 安全模式(授權獲取)
instanceSecurity: 伺服器安全性
privateMode: 私人模式
@ -1820,13 +1824,13 @@ adminCustomCssWarn: 除非你知道它的作用,否則請不要使用此設定
CSS 正常工作。
showUpdates: Firefish 更新時顯示彈出視窗
recommendedInstances: 建議的伺服器
caption: 自動字幕
caption: 自動加上替代文字(alt)
enterSendsMessage: 在 Messaging 中按 Return 發送消息 (如關閉則是 Ctrl + Return)
migrationConfirm: "您確定要將你的帳戶遷移到 {account} 嗎? 一旦這樣做,你將無法復原,而你將無法再次正常使用您的帳戶。\n另外請確保你已將此當前帳戶設置為您要遷移的帳戶。"
customSplashIconsDescription: 每次用戶加載/重新加載頁面時,以換行符號分隔的自定啟動畫面圖標的網址將隨機顯示。請確保圖片位於靜態網址上,最好所有圖片解析度調整為
192x192。
accountMoved: '該使用者已遷移至新帳戶:'
showAds: 顯示廣告
showAds: 顯示社群橫幅
noThankYou: 不用了,謝謝
selectInstance: 選擇伺服器
enableRecommendedTimeline: 啟用推薦時間線
@ -1870,7 +1874,7 @@ hiddenTags: 隱藏主題標籤
indexPosts: 索引貼文
indexNotice: 現在開始索引。 這可能需要一段時間,請不要在一個小時內重啟你的伺服器。
deleted: 已刪除
editNote: 編輯筆記
editNote: 編輯貼文
edited: '於 {date} {time} 編輯'
userSaysSomethingReason: '{name} 說了 {reason}'
allowedInstancesDescription: 要加入聯邦白名單的服務器,每台伺服器用新行分隔(僅適用於私有模式)。
@ -1893,7 +1897,7 @@ listsDesc: 清單可以創建一個只有您指定用戶的時間線。 可以
flagSpeakAsCatDescription: 在喵咪模式下你的貼文會被喵化ヾ(•ω•`)o
antennasDesc: "天線會顯示符合您設置條件的新貼文!\n 可以從時間線訪問它們。"
expandOnNoteClick: 點擊以打開貼文
expandOnNoteClickDesc: 如果禁用,您仍然可以通過右鍵單擊菜單或單擊時間戳來打開貼文。
expandOnNoteClickDesc: 即使停用,您仍然可以從右鍵選單或單擊發文時間來打開貼文。
hiddenTagsDescription: '列出您希望隱藏趨勢和探索的主題標籤(不帶 #)。 隱藏的主題標籤仍然可以通過其他方式發現。'
userSaysSomethingReasonQuote: '{name} 引用了一篇包含 {reason} 的貼文'
silencedInstancesDescription: 列出您想要靜音的伺服器的網址。 您列出的伺服器內的帳戶將被視為“沉默”,只能發出追隨請求,如果不追隨則不能提及本地帳戶。
@ -1944,3 +1948,18 @@ _filters:
withFile: 有檔案
alt: 替代文字
xl: 特大
inputNotMatch: 輸入不一致
delete2faConfirm: 二階段認證(2FA)將被完全刪除。是否繼續?
_dialog:
charactersBelow: 字數不足! 當前 {current} / 限制 {min}
charactersExceeded: 超過字數限制! 當前 {current} / 限制 {max}
_skinTones:
yellow: 黃色
exportZip: 匯出ZIP
_feeds:
atom: Atom
rss: RSS
emojiPackCreator: 表情包的作者
importZip: 匯入ZIP
delete2fa: 停用二階段認證(2FA)
confirm: 確認

View file

@ -1 +1 @@
980addc1fa9dedf79b18792b68feaa0cb21f9aa4
c17fb8217b88ef3e4d4f0756a458e2114ba47088

View file

@ -1,12 +1,12 @@
{
"name": "firefish",
"version": "1.0.5-dev12",
"version": "1.0.4-beta31",
"codename": "aqua",
"repository": {
"type": "git",
"url": "https://code.naskya.net/naskya/firefish"
},
"packageManager": "pnpm@8.7.4",
"packageManager": "pnpm@8.7.6",
"private": true,
"scripts": {
"rebuild": "pnpm run clean && ./scripts/build-greet.sh && pnpm -r --parallel run build && pnpm run gulp",

View file

@ -129,6 +129,7 @@
"tar-stream": "^3.1.6",
"tesseract.js": "^4.1.1",
"tinycolor2": "1.6.0",
"tinyld": "^1.3.4",
"tmp": "0.2.1",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.17",
@ -140,7 +141,7 @@
},
"devDependencies": {
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.75",
"@swc/core": "1.3.78",
"@types/adm-zip": "^0.5.0",
"@types/bcryptjs": "2.4.2",
"@types/escape-regexp": "0.0.1",

View file

@ -0,0 +1,7 @@
declare module "langdetect" {
interface DetectResult {
lang: string;
prob: number;
}
export function detect(words: string): DetectResult[];
}

View file

@ -27,6 +27,7 @@ import {
} from "@/misc/populate-emojis.js";
import { db } from "@/db/postgre.js";
import { IdentifiableError } from "@/misc/identifiable-error.js";
import { detect as detectLanguage_ } from "tinyld";
export async function populatePoll(note: Note, meId: User["id"] | null) {
const poll = await Polls.findOneByOrFail({ noteId: note.id });
@ -201,6 +202,9 @@ export const NoteRepository = db.getRepository(Note).extend({
note.emojis.concat(reactionEmojiNames),
host,
);
const lang =
detectLanguage_(`${note.cw ?? ""}\n${note.text ?? ""}`) ?? "unknown";
const reactionEmoji = await populateEmojis(reactionEmojiNames, host);
const packed: Packed<"Note"> = await awaitAll({
id: note.id,
@ -260,6 +264,7 @@ export const NoteRepository = db.getRepository(Note).extend({
: undefined,
}
: {}),
lang: lang,
});
if (packed.user.isCat && packed.user.speakAsCat && packed.text) {

View file

@ -87,8 +87,12 @@ export default define(meta, paramDef, async (ps, user) => {
.andWhere(
new Brackets((qb) => {
qb.where("note.userId = :meId", { meId: user.id });
if (hasFollowing)
qb.orWhere(`note.userId IN (${followingQuery.getQuery()})`);
if (hasFollowing) {
qb.orWhere(
`note.userId IN (${followingQuery.getQuery()})`,
followingQuery.getParameters(),
);
}
}),
)
.innerJoinAndSelect("note.user", "user")

View file

@ -1,19 +1,13 @@
import { Entity } from "megalodon";
import config from "@/config/index.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { Users, Notes } from "@/models/index.js";
import { IsNull } from "typeorm";
import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from "@/const.js";
export async function getInstance(
response: Entity.Instance,
contact: Entity.Account,
) {
const [meta, totalUsers, totalStatuses] = await Promise.all([
fetchMeta(true),
Users.count({ where: { host: IsNull() } }),
Notes.count({ where: { userHost: IsNull() } }),
]);
const meta = await fetchMeta(true);
return {
uri: response.uri,
@ -27,8 +21,8 @@ export async function getInstance(
version: `3.0.0 (compatible; Firefish ${config.version})`,
urls: response.urls,
stats: {
user_count: await totalUsers,
status_count: await totalStatuses,
user_count: response.stats.user_count,
status_count: response.stats.status_count,
domain_count: response.stats.domain_count,
},
thumbnail: response.thumbnail || "/static-assets/transparent.png",

View file

@ -28,6 +28,7 @@
<script lang="ts" setup>
import { nextTick, onMounted, ref } from "vue";
import { vibrate } from "@/scripts/vibrate";
const props = defineProps<{
type?: "button" | "submit" | "reset";
@ -93,6 +94,8 @@ function onMousedown(evt: MouseEvent): void {
circleCenterY,
);
vibrate(10);
window.setTimeout(() => {
ripple.style.transform = "scale(" + scale / 2 + ")";
}, 1);

View file

@ -23,6 +23,7 @@
<button
v-for="emoji in searchResultCustom"
:key="emoji.id"
v-vibrate="50"
class="_button item"
:title="emoji.name"
tabindex="0"

View file

@ -4,6 +4,7 @@
<div class="title"><slot name="header"></slot></div>
<div class="divider"></div>
<button
v-vibrate="5"
class="_button"
:aria-expanded="showBody"
:aria-controls="bodyId"

View file

@ -69,6 +69,7 @@ import { i18n } from "@/i18n";
import { $i } from "@/account";
import { getUserMenu } from "@/scripts/get-user-menu";
import { useRouter } from "@/router";
import { vibrate } from "@/scripts/vibrate";
const router = useRouter();
@ -157,6 +158,7 @@ async function onClick() {
await os.api("following/create", {
userId: props.user.id,
});
vibrate([30, 40, 100]);
hasPendingFollowRequestFromYou.value = true;
}
}

View file

@ -6,7 +6,7 @@
"
class="hpaizdrt"
:style="bg"
@click.stop="os.pageWindow(instanceInfoUrl)"
@click.stop="openServerInfo"
>
<img class="icon" :src="getInstanceIcon(instance)" aria-hidden="true" />
<span class="name">{{ instance.name }}</span>
@ -19,7 +19,8 @@ import { ref } from "vue";
import { instanceName, version } from "@/config";
import { instance as Instance } from "@/instance";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
import * as os from "@/os";
import { defaultStore } from "@/store";
import { pageWindow } from "@/os";
const props = defineProps<{
instance?: {
@ -93,6 +94,13 @@ function getInstanceIcon(instance): string {
"/client-assets/dummy.png"
);
}
function openServerInfo() {
if (!defaultStore.state.openServerInfo) return;
const instanceInfoUrl =
props.host == null ? "/about" : `/instance-info/${props.host}`;
pageWindow(instanceInfoUrl);
}
</script>
<style lang="scss" scoped>

View file

@ -8,6 +8,7 @@
<div>
<div
ref="itemsEl"
v-vibrate="5"
class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }"
:style="{

View file

@ -6,6 +6,7 @@
ref="el"
v-hotkey="keymap"
v-size="{ max: [500, 350] }"
v-vibrate="5"
:aria-label="accessibleLabel"
class="tkcbzcuz note-container"
:tabindex="!isDeleted ? '-1' : null"
@ -225,9 +226,9 @@
isForeignLanguage &&
translation == null
"
v-tooltip.noDelay.bottom="i18n.ts.translate"
class="button _button"
@click.stop="translate"
v-tooltip.noDelay.bottom="i18n.ts.translate"
>
<i class="ph-translate ph-bold ph-lg"></i>
</button>
@ -353,7 +354,12 @@ const isMyRenote = $i && $i.id === note.value.userId;
const showContent = ref(false);
const isDeleted = ref(false);
const muted = ref(
getWordSoftMute(note.value, $i, defaultStore.state.mutedWords),
getWordSoftMute(
note.value,
$i,
defaultStore.state.mutedWords,
defaultStore.state.mutedLangs,
),
);
const translation = ref(null);
const translating = ref(false);
@ -385,8 +391,8 @@ const isForeignLanguage: boolean =
async function translate_(noteId, targetLang: string) {
return await os.api("notes/translate", {
noteId: noteId,
targetLang: targetLang,
noteId,
targetLang,
});
}

View file

@ -209,7 +209,12 @@ const reactButton = ref<HTMLElement>();
const showContent = ref(false);
const isDeleted = ref(false);
const muted = ref(
getWordSoftMute(note.value, $i, defaultStore.state.mutedWords),
getWordSoftMute(
note.value,
$i,
defaultStore.state.mutedWords,
defaultStore.state.mutedLangs,
),
);
const translation = ref(null);
const translating = ref(false);

View file

@ -130,9 +130,9 @@
isForeignLanguage &&
translation == null
"
v-tooltip.noDelay.bottom="i18n.ts.translate"
class="button _button"
@click.stop="translate"
v-tooltip.noDelay.bottom="i18n.ts.translate"
>
<i class="ph-translate ph-bold ph-lg"></i>
</button>
@ -266,7 +266,12 @@ const appearNote = computed(() =>
);
const isDeleted = ref(false);
const muted = ref(
getWordSoftMute(note.value, $i, defaultStore.state.mutedWords),
getWordSoftMute(
note.value,
$i,
defaultStore.state.mutedWords,
defaultStore.state.mutedLangs,
),
);
const translation = ref(null);
const translating = ref(false);
@ -306,8 +311,8 @@ const isForeignLanguage: boolean =
async function translate_(noteId, targetLang: string) {
return await os.api("notes/translate", {
noteId: noteId,
targetLang: targetLang,
noteId,
targetLang,
});
}

View file

@ -29,7 +29,7 @@
>{{ maxTextLength - textLength }}</span
>
<span v-if="localOnly" class="local-only"
><i class="ph-hand-fist ph-bold ph-lg"></i
><i class="ph-users ph-bold ph-lg"></i
></span>
<button
ref="visibilityButton"
@ -286,6 +286,7 @@ import {
import { uploadFile } from "@/scripts/upload";
import { deepClone } from "@/scripts/clone";
import { preprocess } from "@/scripts/preprocess";
import { vibrate } from "@/scripts/vibrate";
const modal = inject("modal");
@ -948,6 +949,7 @@ async function post() {
text: err.message + "\n" + (err as any).id,
});
});
vibrate([10, 20, 10, 20, 10, 20, 60]);
}
function cancel() {

View file

@ -3,6 +3,7 @@
v-if="count > 0"
ref="buttonRef"
v-ripple="canToggle"
v-vibrate="[10, 30, 40]"
class="hkzvhatu _button"
:class="{
reacted: note.myReaction == reaction,

View file

@ -32,6 +32,7 @@ import { useTooltip } from "@/scripts/use-tooltip";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
import type { MenuItem } from "@/types/menu";
import { vibrate } from "@/scripts/vibrate";
const props = defineProps<{
note: misskey.entities.Note;
@ -194,9 +195,10 @@ const renote = (viaKeyboard = false, ev?: MouseEvent) => {
if (canRenote.value) {
buttonActions.push({
text: `${i18n.ts.renote} (${i18n.ts.local})`,
icon: "ph-hand-fist ph-bold ph-lg",
icon: "ph-users ph-bold ph-lg",
danger: false,
action: () => {
vibrate([30, 30, 60]);
os.api(
"notes/create",
props.note.visibility === "specified"

View file

@ -1,6 +1,7 @@
<template>
<button
v-tooltip.noDelay.bottom="i18n.ts._gallery.like"
v-vibrate="[30, 50, 50]"
class="button _button"
@click.stop="star($event)"
>

View file

@ -2,6 +2,7 @@
<button
ref="buttonRef"
v-tooltip.noDelay.bottom="i18n.ts._gallery.like"
v-vibrate="[30, 50, 50]"
class="button _button"
:class="$style.root"
@click.stop="toggleStar($event)"

View file

@ -19,7 +19,7 @@
<span v-if="note.localOnly" :class="$style.localOnly"
><i
v-tooltip="i18n.ts._visibility.localOnly"
class="ph-hand-fist ph-bold ph-lg"
class="ph-users ph-bold ph-lg"
></i
></span>
</template>

View file

@ -97,7 +97,7 @@
@click="localOnly = !localOnly"
>
<div :class="$style.icon">
<i class="ph-hand-fist ph-bold ph-lg"></i>
<i class="ph-users ph-bold ph-lg"></i>
</div>
<div :class="$style.body">
<span :class="$style.itemTitle">{{

View file

@ -12,6 +12,7 @@
<button
v-if="displayBackButton"
v-tooltip.noDelay="i18n.ts.goBack"
v-vibrate="5"
class="_buttonIcon button icon backButton"
@click.stop="goBack()"
@touchstart="preventDrag"
@ -20,6 +21,7 @@
</button>
<MkAvatar
v-if="narrow && props.displayMyAvatar && $i"
v-vibrate="5"
class="avatar button"
:user="$i"
:disable-preview="true"
@ -77,6 +79,7 @@
v-for="tab in tabs"
:ref="(el) => (tabRefs[tab.key] = el)"
v-tooltip.noDelay="tab.title"
v-vibrate="5"
class="tab _button"
:class="{
active: tab.key != null && tab.key === props.tab,
@ -108,6 +111,7 @@
<template v-for="action in actions">
<button
v-tooltip.noDelay="action.text"
v-vibrate="5"
class="_buttonIcon button"
:class="{ highlighted: action.highlighted }"
@click.stop="action.handler"

View file

@ -12,6 +12,7 @@ import clickAnime from "./click-anime";
import panel from "./panel";
import adaptiveBorder from "./adaptive-border";
import focus from "./focus";
import vibrate from "./vibrate";
export default function (app: App) {
app.directive("userPreview", userPreview);
@ -27,4 +28,5 @@ export default function (app: App) {
app.directive("panel", panel);
app.directive("adaptive-border", adaptiveBorder);
app.directive("focus", focus);
app.directive("vibrate", vibrate);
}

View file

@ -0,0 +1,11 @@
import type { Directive } from "vue";
import { vibrate } from "../scripts/vibrate";
export default {
mounted(el, binding) {
const pattern = (binding.value as VibratePattern) ?? 20;
el.addEventListener("mousedown", () => {
vibrate(pattern);
});
},
} as Directive;

View file

@ -431,7 +431,7 @@ const headerActions = computed(() => [
const headerTabs = computed(() => [
{
key: "local",
icon: "ph-hand-fist ph-bold ph-lg",
icon: "ph-users ph-bold ph-lg",
title: i18n.ts.local,
},
{

View file

@ -46,14 +46,13 @@
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { onMounted, ref } from "vue";
import XForm from "./auth.form.vue";
import MkSignin from "@/components/MkSignin.vue";
import MkKeyValue from "@/components/MkKeyValue.vue";
import * as os from "@/os";
import { login } from "@/account";
import { $i, login } from "@/account";
import { i18n } from "@/i18n";
import { $i } from "@/account";
const props = defineProps<{
token: string;
@ -102,11 +101,7 @@ const accepted = () => {
const isMastodon = !!getUrlParams().mastodon;
if (session.value.app.callbackUrl && isMastodon) {
const redirectUri = decodeURIComponent(getUrlParams().redirect_uri);
if (
!session.value.app.callbackUrl
.split("\n")
.some((p) => p === redirectUri)
) {
if (!session.value.app.callbackUrl.split("\n").includes(redirectUri)) {
state.value = "fetch-session-error";
fetching.value = false;
throw new Error("Callback URI doesn't match registered app");

View file

@ -120,6 +120,7 @@ import {
import * as os from "@/os";
import { stream } from "@/stream";
import * as sound from "@/scripts/sound";
import { vibrate } from "@/scripts/vibrate";
import { i18n } from "@/i18n";
import { $i } from "@/account";
import { defaultStore } from "@/store";
@ -251,6 +252,7 @@ function onDrop(ev: DragEvent): void {
function onMessage(message) {
sound.play("chat");
vibrate([30, 30, 30]);
const _isBottom = isBottomVisible(rootEl.value, 64);

View file

@ -89,6 +89,9 @@
<FormSwitch v-model="detectPostLanguage" class="_formBlock">{{
i18n.ts.detectPostLanguage
}}</FormSwitch>
<FormSwitch v-model="openServerInfo" class="_formBlock">{{
i18n.ts.openServerInfo
}}</FormSwitch>
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@ -139,6 +142,12 @@
class="_formBlock"
>{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch
>
<FormSwitch
v-model="vibrate"
class="_formBlock"
@click="demoVibrate"
>{{ i18n.ts.vibrate }}
</FormSwitch>
<FormRadios v-model="fontSize" class="_formBlock">
<template #label>{{ i18n.ts.fontSize }}</template>
<option :value="null">
@ -340,7 +349,7 @@ import FormSection from "@/components/form/section.vue";
import FormLink from "@/components/form/link.vue";
import MkLink from "@/components/MkLink.vue";
import { langs } from "@/config";
import { defaultStore } from "@/store";
import { ColdDeviceStorage, defaultStore } from "@/store";
import * as os from "@/os";
import { unisonReload } from "@/scripts/unison-reload";
import { i18n } from "@/i18n";
@ -362,6 +371,10 @@ async function reloadAsk() {
unisonReload();
}
function demoVibrate() {
window.navigator.vibrate(100);
}
const overridedDeviceKind = computed(
defaultStore.makeGetterSetter("overridedDeviceKind"),
);
@ -398,6 +411,7 @@ const disableDrawer = computed(defaultStore.makeGetterSetter("disableDrawer"));
const disableShowingAnimatedImages = computed(
defaultStore.makeGetterSetter("disableShowingAnimatedImages"),
);
const vibrate = computed(ColdDeviceStorage.makeGetterSetter("vibrate"));
const loadRawImages = computed(defaultStore.makeGetterSetter("loadRawImages"));
const imageNewTab = computed(defaultStore.makeGetterSetter("imageNewTab"));
const nsfw = computed(defaultStore.makeGetterSetter("nsfw"));
@ -464,6 +478,9 @@ const emphasizeFollowed = computed(
const disableToast = computed(defaultStore.makeGetterSetter("disableToast"));
const hideMyIcon = computed(defaultStore.makeGetterSetter("hideMyIcon"));
const hideMyName = computed(defaultStore.makeGetterSetter("hideMyName"));
const openServerInfo = computed(
defaultStore.makeGetterSetter("openServerInfo"),
);
watch(swipeOnDesktop, () => {
defaultStore.set("swipeOnMobile", true);

View file

@ -9,7 +9,7 @@
<template #empty
><FormInfo>{{ i18n.ts.noUsers }}</FormInfo></template
>
<template #default="{ items }">
<template #default="{ items }" class="_formlinks">
<FormLink
v-for="mute in items"
:key="mute.id"
@ -25,7 +25,7 @@
<template #empty
><FormInfo>{{ i18n.ts.noUsers }}</FormInfo></template
>
<template #default="{ items }">
<template #default="{ items }" class="_formlinks">
<FormLink
v-for="block in items"
:key="block.id"

View file

@ -117,6 +117,8 @@ const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [
"enableEmojiReactions",
"showEmojisInReactionNotifications",
"showTimelineReplies",
"detectPostLanguage",
"openServerInfo",
];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
"lightTheme",
@ -124,6 +126,7 @@ const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
"syncDeviceDarkMode",
"plugins",
"mediaVolume",
"vibrate",
"sound_masterVolume",
"sound_note",
"sound_noteMy",

View file

@ -17,6 +17,17 @@
}}</template
>
</FormTextarea>
<MkInfo class="_formBlock">{{
i18n.ts._wordMute.langDescription
}}</MkInfo>
<FormTextarea v-model="softMutedLangs" class="_formBlock">
<span>{{ i18n.ts._wordMute.muteLangs }}</span>
<template #caption
>{{ i18n.ts._wordMute.muteLangsDescription }}<br />{{
i18n.ts._wordMute.muteLangsDescription2
}}</template
>
</FormTextarea>
</div>
<div v-show="tab === 'hard'">
<MkInfo class="_formBlock"
@ -76,6 +87,7 @@ const render = (mutedWords) =>
const tab = ref("soft");
const softMutedWords = ref(render(defaultStore.state.mutedWords));
const softMutedLangs = ref(render(defaultStore.state.mutedLangs));
const hardMutedWords = ref(render($i!.mutedWords));
const hardWordMutedNotesCount = ref(null);
const changed = ref(false);
@ -88,6 +100,10 @@ watch(softMutedWords, () => {
changed.value = true;
});
watch(softMutedLangs, () => {
changed.value = true;
});
watch(hardMutedWords, () => {
changed.value = true;
});
@ -134,9 +150,10 @@ async function save() {
return lines;
};
let softMutes, hardMutes;
let softMutes, softMLangs, hardMutes;
try {
softMutes = parseMutes(softMutedWords.value, i18n.ts._wordMute.soft);
softMLangs = parseMutes(softMutedLangs.value, i18n.ts._wordMute.lang);
hardMutes = parseMutes(hardMutedWords.value, i18n.ts._wordMute.hard);
} catch (err) {
// already displayed error message in parseMutes
@ -144,6 +161,7 @@ async function save() {
}
defaultStore.set("mutedWords", softMutes);
defaultStore.set("mutedLangs", softMLangs);
await os.api("i/update", {
mutedWords: hardMutes,
});

View file

@ -65,7 +65,7 @@ const visibleUsers = ref([] as Misskey.entities.User[]);
async function init() {
let noteText = "";
if (title.value) noteText += `[ ${title.value} ]\n`;
if (title.value) noteText += `${title.value}\n`;
// Google
if (text?.startsWith(`${title.value}.\n`))
noteText += text.replace(`${title.value}.\n`, "");

View file

@ -6,6 +6,19 @@ export interface Muted {
const NotMuted = { muted: false, matched: [] };
function checkLangMute(
note: NoteLike,
mutedLangs: Array<string | string[]>,
): Muted {
const mutedLangList = new Set(
mutedLangs.reduce((arr, x) => [...arr, ...(Array.isArray(x) ? x : [x])]),
);
if (mutedLangList.has((note.lang?.[0]?.lang || "").split("-")[0])) {
return { muted: true, matched: [note.lang?.[0]?.lang] };
}
return NotMuted;
}
function checkWordMute(
note: NoteLike,
mutedWords: Array<string | string[]>,
@ -62,6 +75,7 @@ export function getWordSoftMute(
note: Record<string, any>,
me: Record<string, any> | null | undefined,
mutedWords: Array<string | string[]>,
mutedLangs: Array<string | string[]>,
): Muted {
// 自分自身
if (me && note.userId === me.id) {
@ -91,6 +105,29 @@ export function getWordSoftMute(
}
}
}
if (mutedLangs.length > 0) {
let noteLangMuted = checkLangMute(note, mutedLangs);
if (noteLangMuted.muted) {
noteLangMuted.what = "note";
return noteLangMuted;
}
if (note.renote) {
let renoteLangMuted = checkLangMute(note, mutedLangs);
if (renoteLangMuted.muted) {
renoteLangMuted.what = note.text == null ? "renote" : "quote";
return renoteLangMuted;
}
}
if (note.reply) {
let replyLangMuted = checkLangMute(note, mutedLangs);
if (replyLangMuted.muted) {
replyLangMuted.what = "reply";
return replyLangMuted;
}
}
}
return NotMuted;
}

View file

@ -239,8 +239,8 @@ export function getNoteMenu(props: {
async function translate_(noteId: number, targetLang: string) {
return await os.api("notes/translate", {
noteId: noteId,
targetLang: targetLang,
noteId,
targetLang,
});
}

View file

@ -2,9 +2,11 @@ import { defineAsyncComponent } from "vue";
import { $i } from "@/account";
import { i18n } from "@/i18n";
import { popup } from "@/os";
import { vibrate } from "@/scripts/vibrate";
export function pleaseLogin(path?: string) {
if ($i) return;
vibrate(100);
popup(
defineAsyncComponent(() => import("@/components/MkSigninDialog.vue")),

View file

@ -0,0 +1,6 @@
import { ColdDeviceStorage } from "@/store";
export function vibrate(pattern: VibratePattern) {
if (!ColdDeviceStorage.get("vibrate") || !window.navigator.vibrate) return;
window.navigator.vibrate(pattern);
}

View file

@ -101,6 +101,10 @@ export const defaultStore = markRaw(
where: "account",
default: [],
},
mutedLangs: {
where: "account",
default: [],
},
mutedAds: {
where: "account",
default: [] as string[],
@ -385,6 +389,10 @@ export const defaultStore = markRaw(
where: "device",
default: false,
},
openServerInfo: {
where: "device",
default: true,
},
}),
);
@ -414,6 +422,7 @@ export class ColdDeviceStorage {
syncDeviceDarkMode: true,
plugins: [] as Plugin[],
mediaVolume: 0.5,
vibrate: true,
sound_masterVolume: 0.3,
sound_note: { type: "none", volume: 0 },
sound_noteMy: { type: "syuilo/up", volume: 1 },

View file

@ -25,6 +25,7 @@
<button
v-if="!isDesktop && !isMobile"
v-vibrate="5"
class="widgetButton _button"
@click="widgetsShowing = true"
>
@ -33,6 +34,7 @@
<div v-if="isMobile" class="buttons">
<button
v-vibrate="5"
:aria-label="i18n.t('menu')"
class="button nav _button"
@click="drawerMenuShowing = true"
@ -48,6 +50,7 @@
</div>
</button>
<button
v-vibrate="5"
:aria-label="i18n.t('home')"
class="button home _button"
@click="
@ -65,6 +68,7 @@
</div>
</button>
<button
v-vibrate="5"
:aria-label="i18n.t('notifications')"
class="button notifications _button"
@click="
@ -73,6 +77,7 @@
"
>
<div
v-vibrate="5"
class="button-wrapper"
:class="buttonAnimIndex === 1 ? 'on' : ''"
>
@ -87,6 +92,7 @@
</button>
<button
v-if="useAccountButton"
v-vibrate="5"
:aria-label="i18n.t('accounts')"
class="button messaging _button"
@click="openAccountMenu"
@ -100,6 +106,7 @@
</button>
<button
v-else
v-vibrate="5"
:aria-label="i18n.t('messaging')"
class="button messaging _button"
@click="
@ -122,6 +129,7 @@
</button>
<button
v-if="useReloadButton"
v-vibrate="5"
:aria-label="i18n.t('reload')"
class="button widget _button"
@click="reload"
@ -132,6 +140,7 @@
</button>
<button
v-else
v-vibrate="5"
:aria-label="i18n.t('_deck._columns.widgets')"
class="button widget _button"
@click="widgetsShowing = true"
@ -144,6 +153,7 @@
<button
v-if="isMobile && mainRouter.currentRoute.value.name === 'index'"
v-vibrate="5"
ref="postButton"
:aria-label="i18n.t('note')"
class="postButton button post _button"
@ -266,7 +276,12 @@ provideMetadataReceiver((info) => {
const menuIndicated = computed(() => {
for (const def in navbarItemDef) {
if (def === "notifications") continue; //
if (def === "notifications") continue; // Notifications & Messaging are bottom nav buttons and thus shouldn't be highlighted in the sidebar
if (
!defaultStore.state.replaceChatButtonWithAccountButton &&
def === "messaging"
)
continue;
if (navbarItemDef[def].indicated) return true;
}
return false;

View file

@ -23,7 +23,8 @@
"@microsoft/api-extractor": "^7.36.0",
"@microsoft/api-documenter": "^7.22.21",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.62",
"@swc/core": "1.3.78",
"@types/jest": "^27.4.0",
"@types/node": "20.3.1",
"mock-socket": "^9.0.8",
"ts-node": "10.4.0",

View file

@ -10,7 +10,7 @@
},
"devDependencies": {
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.62",
"@swc/core": "1.3.78",
"@swc/core-android-arm64": "1.3.11",
"firefish-js": "workspace:*",
"idb-keyval": "^6.2.1",

File diff suppressed because it is too large Load diff