Firefish v1.0.5-dev17

This commit is contained in:
naskya 2023-09-22 14:02:48 +09:00
parent 0526cfbb2a
commit 9f66a25edd
Signed by: naskya
GPG key ID: 164DFF24E2D40139
42 changed files with 1325 additions and 235 deletions

View file

@ -28,6 +28,10 @@ Machine learning model for sensitive images by Infinite Red, Inc.
License: MIT License: MIT
https://github.com/infinitered/nsfwjs/blob/master/LICENSE https://github.com/infinitered/nsfwjs/blob/master/LICENSE
Chiptune2.js by Simon Gündling
License: MIT
https://github.com/deskjet/chiptune2.js#license
Licenses for all softwares and software libraries installed via the Node Package Manager ("npm") can be found by running the following shell command in the root directory of this repository: Licenses for all softwares and software libraries installed via the Node Package Manager ("npm") can be found by running the following shell command in the root directory of this repository:
pnpm licenses list pnpm licenses list

View file

@ -1179,7 +1179,7 @@ emptyToDisableSmtpAuth: Deixa el nom d'usuari i la contrasenya sense emplenar pe
desactivar la verificació SMTP desactivar la verificació SMTP
smtpSecureInfo: Desactiva això quant facis servir STARTTLS smtpSecureInfo: Desactiva això quant facis servir STARTTLS
testEmail: Envia un correu electrònic de verificació testEmail: Envia un correu electrònic de verificació
wordMute: Silenciar paraules wordMute: Paraules i llenguatge silenciats
regexpError: Error a la Expressió Regular regexpError: Error a la Expressió Regular
regexpErrorDescription: 'Hi ha un error a la expressió regular a la línea {line} de regexpErrorDescription: 'Hi ha un error a la expressió regular a la línea {line} de
la teva {tab} de paraules silenciades:' la teva {tab} de paraules silenciades:'
@ -2040,6 +2040,13 @@ _wordMute:
s'afegeixin a la línia de temps. A més, aquestes publicacions no s'afegiran a s'afegeixin a la línia de temps. A més, aquestes publicacions no s'afegiran a
la línia de temps encara que es modifiquin les condicions. la línia de temps encara que es modifiquin les condicions.
mutedNotes: Publicacions silenciades mutedNotes: Publicacions silenciades
muteLangsDescription2: Fes servir el codi del l'idioma. Per exemple en, fr, ja,
zh.
lang: Idioma
langDescription: Amagar les publicacions que coincideixin amb l'idioma a la línia
de temps.
muteLangs: Llenguatges silenciats
muteLangsDescription: Separar amb espais o línies no es per una condició OR.
_auth: _auth:
shareAccessAsk: Estàs segur que vols autoritzar aquesta aplicació per accedir al shareAccessAsk: Estàs segur que vols autoritzar aquesta aplicació per accedir al
teu compte? teu compte?
@ -2192,3 +2199,4 @@ indexable: Indexable
languageForTranslation: Idioma de traducció d'articles languageForTranslation: Idioma de traducció d'articles
openServerInfo: Mostra la informació del servidor fent clic al símbol del servidor openServerInfo: Mostra la informació del servidor fent clic al símbol del servidor
en un missatge en un missatge
vibrate: Activar vibracions

View file

@ -132,6 +132,7 @@ rememberNoteVisibility: "Remember post visibility settings"
attachCancel: "Remove attachment" attachCancel: "Remove attachment"
markAsSensitive: "Mark as NSFW" markAsSensitive: "Mark as NSFW"
unmarkAsSensitive: "Unmark as NSFW" unmarkAsSensitive: "Unmark as NSFW"
clickToShowPatterns: "Click to show module patterns"
enterFileName: "Enter filename" enterFileName: "Enter filename"
mute: "Mute" mute: "Mute"
unmute: "Unmute" unmute: "Unmute"

View file

@ -637,7 +637,7 @@ emptyToDisableSmtpAuth: "Kosongkan nama pengguna dan kata sandi untuk menonaktif
smtpSecure: "Gunakan SSL/TLS implisit untuk koneksi SMTP" smtpSecure: "Gunakan SSL/TLS implisit untuk koneksi SMTP"
smtpSecureInfo: "Matikan ini ketika menggunakan STARTTLS" smtpSecureInfo: "Matikan ini ketika menggunakan STARTTLS"
testEmail: "Tes pengiriman surel" testEmail: "Tes pengiriman surel"
wordMute: "Bisukan kata" wordMute: "Bisukan kata dan bahasa"
regexpError: "Kesalahan ekspresi reguler" regexpError: "Kesalahan ekspresi reguler"
regexpErrorDescription: "Galat terjadi pada baris {line} ekspresi reguler dari {tab} regexpErrorDescription: "Galat terjadi pada baris {line} ekspresi reguler dari {tab}
kata yang dibisukan:" kata yang dibisukan:"
@ -1135,6 +1135,12 @@ _wordMute:
soft: "Lembut" soft: "Lembut"
hard: "Keras" hard: "Keras"
mutedNotes: "Postingan yang dibisukan" mutedNotes: "Postingan yang dibisukan"
muteLangsDescription2: Gunakan kode bahasa misalnya en, fr, ja, zh.
lang: Bahasa
langDescription: Sembunyikan postingan yang cocok dengan bahasa yang ditetapkan
dari timeline.
muteLangs: Bahasa yang dibisukan
muteLangsDescription: Pisahkan dengan spasi atau jeda baris untuk kondisi ATAU.
_instanceMute: _instanceMute:
instanceMuteDescription: "Pengaturan ini akan membisukan postingan/pembagian apa instanceMuteDescription: "Pengaturan ini akan membisukan postingan/pembagian apa
saja dari server yang terdaftar, termasuk pengguna yang membalas pengguna lain saja dari server yang terdaftar, termasuk pengguna yang membalas pengguna lain
@ -2175,3 +2181,4 @@ indexable: Dapat diindeks
languageForTranslation: Bahasa terjemahan kiriman languageForTranslation: Bahasa terjemahan kiriman
openServerInfo: Tampilkan informasi server dengan mengeklik ticker server di sebuah openServerInfo: Tampilkan informasi server dengan mengeklik ticker server di sebuah
kiriman kiriman
vibrate: Putar getaran

View file

@ -120,6 +120,7 @@ rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas"
attachCancel: "Remover anexo" attachCancel: "Remover anexo"
markAsSensitive: "Marcar como sensível" markAsSensitive: "Marcar como sensível"
unmarkAsSensitive: "Desmarcar como sensível" unmarkAsSensitive: "Desmarcar como sensível"
clickToShowPatterns: "Clique para mostrar os padrões do módulo"
enterFileName: "Digite o nome do ficheiro" enterFileName: "Digite o nome do ficheiro"
mute: "Silenciar" mute: "Silenciar"
unmute: "Dessilenciar" unmute: "Dessilenciar"

View file

@ -285,7 +285,7 @@ pinnedPagesDescription: Bu sunucunun üst kısmına sabitlemek istediğiniz Sayf
yollarını satır sonundan ayırarak girin. yollarını satır sonundan ayırarak girin.
enableHcaptcha: hCaptcha'yı Aktif Et enableHcaptcha: hCaptcha'yı Aktif Et
notifyAntenna: Yeni gönderileri bildir notifyAntenna: Yeni gönderileri bildir
recentlyUpdatedUsers: En son aktif kullanıcılar recentlyUpdatedUsers: En son aktif olan kullanıcılar
about: Hakkında about: Hakkında
twoStepAuthentication: İki-adımlı doğrulama twoStepAuthentication: İki-adımlı doğrulama
securityKeyName: Anahtar ismi securityKeyName: Anahtar ismi

View file

@ -486,15 +486,16 @@ hideThisNote: "隐藏这条帖子"
showFeaturedNotesInTimeline: "在时间线上显示热门推荐" showFeaturedNotesInTimeline: "在时间线上显示热门推荐"
objectStorage: "对象存储" objectStorage: "对象存储"
useObjectStorage: "使用对象存储" useObjectStorage: "使用对象存储"
objectStorageBaseUrl: "Base URL" objectStorageBaseUrl: "根 URL"
objectStorageBaseUrlDesc: "用于引用的 URL。如果您正在使用 CDN 或反向代理,请指定其 URL。\n例如S3“https://<bucket>.s3.amazonaws.com”GCS“https://storage.googleapis.com/<bucket>”,其它同理。" objectStorageBaseUrlDesc: "用于引用的 URL。如果您正在使用 CDN 或反向代理,请指定其 URL。\n例如S3\"https://<bucket>.s3.amazonaws.com\"\
GCS\"https://storage.googleapis.com/<bucket>\",其它同理。"
objectStorageBucket: "存储桶" objectStorageBucket: "存储桶"
objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。" objectStorageBucketDesc: "请指定使用的对象存储服务的存储桶名称。"
objectStoragePrefix: "前缀" objectStoragePrefix: "前缀"
objectStoragePrefixDesc: "文件将存储在此前缀的目录下。" objectStoragePrefixDesc: "文件将存储在此前缀的目录下。"
objectStorageEndpoint: "Endpoint" objectStorageEndpoint: "端点 (Endpoint)"
objectStorageEndpointDesc: "如果您使用 AWS S3 请留空。否则请根据您使用的服务商的说明来进行设置,指定 Endpoint 形式为 objectStorageEndpointDesc: "如果您使用 AWS S3 请留空。否则请根据您使用的服务商的说明来进行设置,指定端点 (Endpoint)
\"<host>\" 或 \"<host>:<port>\"。" 形式为 \"<host>\" 或 \"<host>:<port>\"。"
objectStorageRegion: "可用区" objectStorageRegion: "可用区"
objectStorageRegionDesc: "指定一个可用区,例如 \"xx-east-1\"。 如果您的对象存储服务没有可用区概念,请将其留空或填写 \"\ objectStorageRegionDesc: "指定一个可用区,例如 \"xx-east-1\"。 如果您的对象存储服务没有可用区概念,请将其留空或填写 \"\
us-east-1\"。\n对于 Cloudflare R2可以填为 \"auto\"。" us-east-1\"。\n对于 Cloudflare R2可以填为 \"auto\"。"
@ -502,7 +503,7 @@ objectStorageUseSSL: "使用 SSL"
objectStorageUseSSLDesc: "如果不使用 HTTPS 进行 API 连接,请关闭" objectStorageUseSSLDesc: "如果不使用 HTTPS 进行 API 连接,请关闭"
objectStorageUseProxy: "使用代理" objectStorageUseProxy: "使用代理"
objectStorageUseProxyDesc: "如果您不使用代理进行 API 连接,请将其关闭" objectStorageUseProxyDesc: "如果您不使用代理进行 API 连接,请将其关闭"
objectStorageSetPublicRead: "上传时设置为 public-read" objectStorageSetPublicRead: "上传时设置为 \"public-read\""
serverLogs: "服务器日志" serverLogs: "服务器日志"
deleteAll: "全部删除" deleteAll: "全部删除"
showFixedPostForm: "在时间线顶部显示发帖框" showFixedPostForm: "在时间线顶部显示发帖框"
@ -599,7 +600,7 @@ emptyToDisableSmtpAuth: "留空用户名和密码以禁用 SMTP 验证"
smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS" smtpSecure: "在 SMTP 连接中使用隐式 SSL / TLS"
smtpSecureInfo: "使用 STARTTLS 时关闭" smtpSecureInfo: "使用 STARTTLS 时关闭"
testEmail: "邮件发送测试" testEmail: "邮件发送测试"
wordMute: "文字过滤" wordMute: "文字和语言过滤"
regexpError: "正则表达式错误" regexpError: "正则表达式错误"
regexpErrorDescription: "{tab} 文字过滤的第 {line} 行的正则表达式有错误:" regexpErrorDescription: "{tab} 文字过滤的第 {line} 行的正则表达式有错误:"
instanceMute: "服务器静音" instanceMute: "服务器静音"
@ -608,7 +609,7 @@ makeActive: "启用"
display: "显示" display: "显示"
copy: "复制" copy: "复制"
metrics: "指标" metrics: "指标"
overview: "服务器概况" overview: "概况"
logs: "日志" logs: "日志"
delayed: "滞后" delayed: "滞后"
database: "数据库" database: "数据库"
@ -745,7 +746,7 @@ unlikeConfirm: "取消赞?"
fullView: "全屏" fullView: "全屏"
quitFullView: "退出全屏" quitFullView: "退出全屏"
addDescription: "添加描述" addDescription: "添加描述"
userPagePinTip: "在帖子的菜单中选择“置顶”,即可显示该条帖子。" userPagePinTip: "在帖子的菜单中选择「置顶」,即可在此显示该条帖子。"
notSpecifiedMentionWarning: "有未指定的提及" notSpecifiedMentionWarning: "有未指定的提及"
info: "关于" info: "关于"
userInfo: "用户信息" userInfo: "用户信息"
@ -805,7 +806,7 @@ accountDeletionInProgress: "正在删除账号"
usernameInfo: "在服务器上唯一标识您的账号的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。" usernameInfo: "在服务器上唯一标识您的账号的名称。您可以使用字母 (a ~ z, A ~ Z)、数字 (0 ~ 9) 和下划线 (_)。用户名以后不能更改。"
aiChanMode: "小蓝模式" aiChanMode: "小蓝模式"
keepCw: "保留内容警告" keepCw: "保留内容警告"
pubSub: "推送 (Pub)/订阅 (Sub) 账号" pubSub: "推送 (Pub) / 订阅 (Sub) 账号"
lastCommunication: "最近通信" lastCommunication: "最近通信"
resolved: "已解决" resolved: "已解决"
unresolved: "未解决" unresolved: "未解决"
@ -1114,7 +1115,7 @@ _wordMute:
muteLangs: "过滤语言" muteLangs: "过滤语言"
muteWordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。" muteWordsDescription: "AND 条件用空格分隔OR 条件用换行符分隔。"
muteWordsDescription2: "将关键字用斜线括起来表示正则表达式。" muteWordsDescription2: "将关键字用斜线括起来表示正则表达式。"
muteLangsDescription: "OR 条件用空格,换行符分隔" muteLangsDescription: "OR 条件用空格或换行符分隔。"
muteLangsDescription2: "使用语言代码。例: en, fr, ja, zh." muteLangsDescription2: "使用语言代码。例: en, fr, ja, zh."
softDescription: "隐藏时间线中指定条件的帖子。" softDescription: "隐藏时间线中指定条件的帖子。"
langDescription: "从时间线中隐藏与设置语言匹配的帖子。" langDescription: "从时间线中隐藏与设置语言匹配的帖子。"
@ -1840,8 +1841,8 @@ customMOTD: 自定义 MOTD启动屏幕消息
sendPushNotificationReadMessageCaption: 会短暂显示 "{emptyPushNotificationMessage}" 的通知,如果启用,可能会增加您的设备的耗电量。 sendPushNotificationReadMessageCaption: 会短暂显示 "{emptyPushNotificationMessage}" 的通知,如果启用,可能会增加您的设备的耗电量。
adminCustomCssWarn: 仅当您知道此设置的作用时才应使用它。输入不正确的值可能会导致每个人的客户端停止正常运行。请在用户设置中进行测试来确保您的 CSS adminCustomCssWarn: 仅当您知道此设置的作用时才应使用它。输入不正确的值可能会导致每个人的客户端停止正常运行。请在用户设置中进行测试来确保您的 CSS
正常工作。 正常工作。
customMOTDDescription: 自定义 MOTD启动屏幕消息一行一个每次用户加载/刷新页面时都会随机显示。 customMOTDDescription: 自定义 MOTD启动屏幕消息一行一个每次用户加载 / 重新加载页面时都会随机显示。
customSplashIconsDescription: 用换行符隔开的自定义启动屏幕图标的 URL在用户每次加载/重新载入页面时随机显示。请确保图片是在一个静态的 customSplashIconsDescription: 用换行符隔开的自定义启动屏幕图标的 URL在用户每次加载 / 重新加载页面时随机显示。请确保图片是在一个静态的
URL 上,最好全部调整为 192x192 的大小。 URL 上,最好全部调整为 192x192 的大小。
recommendedInstancesDescription: 推荐的服务器一行一个,它们将出现在推荐时间线中。 recommendedInstancesDescription: 推荐的服务器一行一个,它们将出现在推荐时间线中。
splash: 启动画面 splash: 启动画面
@ -1864,7 +1865,7 @@ customSplashIcons: 自定义启动屏幕图标urls
alt: 替代文字 alt: 替代文字
pushNotificationNotSupported: 您的浏览器或者服务器不支持推送通知 pushNotificationNotSupported: 您的浏览器或者服务器不支持推送通知
showAds: 显示社区横幅 showAds: 显示社区横幅
enterSendsMessage: 按回车键发送信息(关闭则是 Ctrl + Retun 发送) enterSendsMessage: 按回车键发送信息(关闭则是 Ctrl + Return 发送)
recommendedInstances: 推荐服务器 recommendedInstances: 推荐服务器
updateAvailable: 可能有可用更新! updateAvailable: 可能有可用更新!
swipeOnMobile: 允许在页面之间滑动 swipeOnMobile: 允许在页面之间滑动
@ -1977,7 +1978,7 @@ confirm: 确认
importZip: 导入 ZIP importZip: 导入 ZIP
exportZip: 导出 ZIP exportZip: 导出 ZIP
emojiPackCreator: 表情包创建工具 emojiPackCreator: 表情包创建工具
objectStorageS3ForcePathStyleDesc: 打开此选项可构建格式为 's3.amazonaws.com/<bucket>/' 而非 '<bucket>.s3.amazonaws.com' objectStorageS3ForcePathStyleDesc: 打开此选项可构建格式为 "s3.amazonaws.com/<bucket>/" 而非 "<bucket>.s3.amazonaws.com"
的端点 URL。 的端点 URL。
objectStorageS3ForcePathStyle: 使用基于路径的端点 URL objectStorageS3ForcePathStyle: 使用基于路径的端点 URL
delete2fa: 禁用 2FA delete2fa: 禁用 2FA
@ -1990,3 +1991,5 @@ detectPostLanguage: 自动检测语言,并显示外文帖子的翻译按钮
indexableDescription: 允许内置搜索显示您的公开帖子 indexableDescription: 允许内置搜索显示您的公开帖子
indexable: 可索引的 indexable: 可索引的
languageForTranslation: 帖子翻译语言 languageForTranslation: 帖子翻译语言
vibrate: 播放振动
openServerInfo: 点击帖子上的服务器滚动条时显示服务器信息

View file

@ -12,7 +12,7 @@ ok: "OK"
gotIt: "知道了!" gotIt: "知道了!"
cancel: "取消" cancel: "取消"
enterUsername: "輸入使用者名稱" enterUsername: "輸入使用者名稱"
renotedBy: "{user} 轉了" renotedBy: "{user} 轉了"
noNotes: "無貼文" noNotes: "無貼文"
noNotifications: "沒有通知" noNotifications: "沒有通知"
instance: "伺服器" instance: "伺服器"
@ -325,7 +325,7 @@ connectService: "己連結"
disconnectService: "己斷開" disconnectService: "己斷開"
enableLocalTimeline: "開啟本地時間線" enableLocalTimeline: "開啟本地時間線"
enableGlobalTimeline: "啟用公開時間線" enableGlobalTimeline: "啟用公開時間線"
disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和版主始終可以訪問所有的時間線。" disablingTimelinesInfo: "即使您關閉了時間線功能,管理員和板主仍可訪問所有的時間線。"
registration: "註冊" registration: "註冊"
enableRegistration: "開啟新使用者註冊" enableRegistration: "開啟新使用者註冊"
invite: "邀請" invite: "邀請"
@ -801,7 +801,7 @@ translatedFrom: "從 {x} 翻譯"
accountDeletionInProgress: "正在刪除帳戶" accountDeletionInProgress: "正在刪除帳戶"
usernameInfo: "在伺服器上您的帳戶是唯一的識別名稱。您可以使用字母 (a ~ z, A ~ Z)、數字 (0 ~ 9) 和下底線 (_)。之後帳戶名是不能更改的。" usernameInfo: "在伺服器上您的帳戶是唯一的識別名稱。您可以使用字母 (a ~ z, A ~ Z)、數字 (0 ~ 9) 和下底線 (_)。之後帳戶名是不能更改的。"
aiChanMode: "小藍模式" aiChanMode: "小藍模式"
keepCw: "保持CW" keepCw: "保持內容警告"
pubSub: "Pub/Sub 帳戶" pubSub: "Pub/Sub 帳戶"
lastCommunication: "最近的通信" lastCommunication: "最近的通信"
resolved: "已解決" resolved: "已解決"
@ -1068,7 +1068,7 @@ _mfm:
position: 位置 position: 位置
alwaysPlay: 自動播放所有MFM動畫 alwaysPlay: 自動播放所有MFM動畫
positionDescription: 按指定數量移動內容。 positionDescription: 按指定數量移動內容。
advancedDescription: 如果禁用,則僅允許基本標記,除非正在播放 MFM 動畫 advancedDescription: 如果停用僅顯示基礎MFM及正在播放的MFM動畫
advanced: 高級MFM advanced: 高級MFM
fade: 淡出 fade: 淡出
foreground: 文字顏色 foreground: 文字顏色
@ -1115,6 +1115,11 @@ _wordMute:
soft: "軟性靜音" soft: "軟性靜音"
hard: "硬性靜音" hard: "硬性靜音"
mutedNotes: "已靜音的貼文" mutedNotes: "已靜音的貼文"
muteLangsDescription2: '使用語言代碼。例: en, fr, ja, zh.'
lang: 語言
langDescription: 將指定語言的貼文從時間線中隱藏。
muteLangs: 被靜音的語言
muteLangsDescription: OR條件以空格或換行進行分隔。
_instanceMute: _instanceMute:
instanceMuteDescription: "包括對被靜音伺服器上的用戶的回覆,被設定的伺服器上所有貼文及轉發都會被靜音。" instanceMuteDescription: "包括對被靜音伺服器上的用戶的回覆,被設定的伺服器上所有貼文及轉發都會被靜音。"
instanceMuteDescription2: "設定時以換行進行分隔" instanceMuteDescription2: "設定時以換行進行分隔"
@ -1356,9 +1361,9 @@ _cw:
files: "{count} 個檔案" files: "{count} 個檔案"
_poll: _poll:
noOnlyOneChoice: "至少需要兩個選項" noOnlyOneChoice: "至少需要兩個選項"
choiceN: "選{n}" choiceN: "選{n}"
noMore: "沒辦法再添加選項了" noMore: "沒辦法再添加選項了"
canMultipleVote: "可以多次投票" canMultipleVote: "允許複選"
expiration: "期限" expiration: "期限"
infinite: "無期限" infinite: "無期限"
at: "結束時間" at: "結束時間"
@ -1367,7 +1372,7 @@ _poll:
deadlineTime: "小時" deadlineTime: "小時"
duration: "時長" duration: "時長"
votesCount: "{n}票" votesCount: "{n}票"
totalVotes: "一共{n}票" totalVotes: "總計{n}票"
vote: "投票" vote: "投票"
showResult: "顯示結果" showResult: "顯示結果"
voted: "已投票" voted: "已投票"
@ -1778,6 +1783,7 @@ _notification:
reply: "回覆" reply: "回覆"
renote: "轉發" renote: "轉發"
reacted: 對您的貼文做出了反應 reacted: 對您的貼文做出了反應
renoted: 轉發了您的貼文
_deck: _deck:
alwaysShowMainColumn: "總是顯示主欄" alwaysShowMainColumn: "總是顯示主欄"
columnAlign: "對齊欄位" columnAlign: "對齊欄位"
@ -1864,9 +1870,10 @@ silencedInstances: 已靜音的伺服器
silenced: 已靜音 silenced: 已靜音
_experiments: _experiments:
title: 試驗功能 title: 試驗功能
enablePostImports: 啟用匯入貼文的功能
findOtherInstance: 找找另一個伺服器 findOtherInstance: 找找另一個伺服器
noGraze: 瀏覽器擴展 "Graze for Mastodon" 會與Firefish發生衝突請停用該擴展。 noGraze: 瀏覽器擴展 "Graze for Mastodon" 會與Firefish發生衝突請停用該擴展。
userSaysSomethingReasonRenote: '{name} 轉了包含 {reason} 的貼文' userSaysSomethingReasonRenote: '{name} 轉了包含 {reason} 的貼文'
pushNotificationNotSupported: 你的瀏覽器或伺服器不支援推送通知 pushNotificationNotSupported: 你的瀏覽器或伺服器不支援推送通知
accessibility: 輔助功能 accessibility: 輔助功能
userSaysSomethingReasonReply: '{name} 回覆了包含 {reason} 的貼文' userSaysSomethingReasonReply: '{name} 回覆了包含 {reason} 的貼文'
@ -1910,7 +1917,7 @@ channelFederationWarn: 頻道功能尚未與聯邦宇宙連動
swipeOnMobile: 允許以滑動在頁面之間切換 swipeOnMobile: 允許以滑動在頁面之間切換
sendPushNotificationReadMessage: 閱讀相關通知或消息後刪除推送通知 sendPushNotificationReadMessage: 閱讀相關通知或消息後刪除推送通知
image: 圖片 image: 圖片
seperateRenoteQuote: 別獨立的轉傳及引用按鈕 seperateRenoteQuote: 開轉發及引用的按鈕
clipsDesc: 摘錄就像一個可以分享的書籤。 你可以從每個貼文的菜單創建新摘錄或將貼文加入已有的摘錄。 clipsDesc: 摘錄就像一個可以分享的書籤。 你可以從每個貼文的菜單創建新摘錄或將貼文加入已有的摘錄。
noteId: 貼文 ID noteId: 貼文 ID
sendModMail: 發送審核通知 sendModMail: 發送審核通知
@ -1920,7 +1927,7 @@ reactionPickerSkinTone: 首選表情符號膚色
indexFromDescription: 留空以索引每個貼文 indexFromDescription: 留空以索引每個貼文
preventAiLearning: 防止 AI 機器人抓取 preventAiLearning: 防止 AI 機器人抓取
preventAiLearningDescription: 請求第三方 AI 語言模型不要研究您上傳的內容,例如貼文和圖像。 preventAiLearningDescription: 請求第三方 AI 語言模型不要研究您上傳的內容,例如貼文和圖像。
indexFrom: 從貼文 ID 開始的索引 indexFrom: 建立此貼文ID以後的索引
isLocked: 該帳戶已獲得以下批准 isLocked: 該帳戶已獲得以下批准
isModerator: 板主 isModerator: 板主
isAdmin: 管理員 isAdmin: 管理員
@ -1928,7 +1935,7 @@ isPatron: Firefish 項目贊助者
silencedWarning: 顯示此頁面是因為這些使用者來自您伺服器管理員已靜音的伺服器,因此他們可能是垃圾訊息。 silencedWarning: 顯示此頁面是因為這些使用者來自您伺服器管理員已靜音的伺服器,因此他們可能是垃圾訊息。
signupsDisabled: 該伺服器上的註冊當前已被禁用,但您隨時可以在另一台伺服器上註冊!或是您有該伺服器的邀請碼,請在下面輸入。 signupsDisabled: 該伺服器上的註冊當前已被禁用,但您隨時可以在另一台伺服器上註冊!或是您有該伺服器的邀請碼,請在下面輸入。
showPopup: 通過彈出式視窗通知用戶 showPopup: 通過彈出式視窗通知用戶
showWithSparkles: 閃閃發光的顯示 showWithSparkles: 讓標題閃閃發光
youHaveUnreadAnnouncements: 您有未讀的公告 youHaveUnreadAnnouncements: 您有未讀的公告
donationLink: 連結到贊助頁面 donationLink: 連結到贊助頁面
neverShow: 不再顯示 neverShow: 不再顯示
@ -1963,3 +1970,11 @@ emojiPackCreator: 表情包的作者
importZip: 匯入ZIP importZip: 匯入ZIP
delete2fa: 停用二階段認證(2FA) delete2fa: 停用二階段認證(2FA)
confirm: 確認 confirm: 確認
deletePasskeysConfirm: 此帳號的所有通行密鑰及安全密鑰將被完全刪除。此動作無法復原,是否繼續?
deletePasskeys: 刪除通行密鑰
detectPostLanguage: 自動判定貼文的語言,並在外文貼文顯示翻譯按鈕
indexableDescription: 允許內建搜尋引擎顯示您的公開貼文
addRe: 在回覆有內容警告的貼文時,在標題前面加上 "re:"
vibrate: 播放振動
openServerInfo: 點擊貼文中的伺服器名稱以顯示伺服器資訊
languageForTranslation: 貼文翻譯語言

View file

@ -1 +1 @@
c17fb8217b88ef3e4d4f0756a458e2114ba47088 2cd036b102edbf63fadb68e3841e4c912032f993

View file

@ -1,6 +1,6 @@
{ {
"name": "firefish", "name": "firefish",
"version": "1.0.4-beta31", "version": "1.0.5-dev17",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -0,0 +1,13 @@
export class AddPostLang1695334243217 {
name = "AddPostLang1695334243217";
async up(queryRunner) {
await queryRunner.query(
`ALTER TABLE "note" ADD "lang" character varying(10)`,
);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "lang"`);
}
}

View file

@ -68,6 +68,15 @@ export const FILE_TYPE_BROWSERSAFE = [
"audio/x-flac", "audio/x-flac",
"audio/flac", "audio/flac",
"audio/vnd.wave", "audio/vnd.wave",
"audio/mod",
"audio/x-mod",
"audio/s3m",
"audio/x-s3m",
"audio/xm",
"audio/x-xm",
"audio/it",
"audio/x-it",
]; ];
/* /*
https://github.com/sindresorhus/file-type/blob/main/supported.js https://github.com/sindresorhus/file-type/blob/main/supported.js

View file

@ -110,9 +110,8 @@ export function fromHtml(html: string, hashtagNames?: string[]): string {
} }
case "h1": { case "h1": {
text += "【";
appendChildren(node.childNodes); appendChildren(node.childNodes);
text += "\n"; text += "\n";
break; break;
} }

View file

@ -1,28 +0,0 @@
export default function (reaction: string): string {
switch (reaction) {
case "like":
return "👍";
case "love":
return "❤️";
case "laugh":
return "😆";
case "hmm":
return "🤔";
case "surprise":
return "😮";
case "congrats":
return "🎉";
case "angry":
return "💢";
case "confused":
return "😥";
case "rip":
return "😇";
case "pudding":
return "🍮";
case "star":
return "⭐";
default:
return reaction;
}
}

View file

@ -4,58 +4,22 @@ import { Emojis } from "@/models/index.js";
import { toPunyNullable } from "./convert-host.js"; import { toPunyNullable } from "./convert-host.js";
import { IsNull } from "typeorm"; import { IsNull } from "typeorm";
const legacies = new Map([
["like", "👍"],
["love", "❤️"],
["laugh", "😆"],
["hmm", "🤔"],
["surprise", "😮"],
["congrats", "🎉"],
["angry", "💢"],
["confused", "😥"],
["rip", "😇"],
["pudding", "🍮"],
["star", "⭐"],
]);
export async function getFallbackReaction() { export async function getFallbackReaction() {
const meta = await fetchMeta(); const meta = await fetchMeta();
return meta.defaultReaction; return meta.defaultReaction;
} }
export function convertLegacyReactions(reactions: Record<string, number>) { export function convertReactions(reactions: Record<string, number>) {
const _reactions = new Map(); const result = new Map();
const decodedReactions = new Map();
for (const reaction in reactions) { for (const reaction in reactions) {
if (reactions[reaction] <= 0) continue; if (reactions[reaction] <= 0) continue;
let decodedReaction; const decoded = decodeReaction(reaction).reaction;
if (decodedReactions.has(reaction)) { result.set(decoded, (result.get(decoded) || 0) + reactions[reaction]);
decodedReaction = decodedReactions.get(reaction);
} else {
decodedReaction = decodeReaction(reaction);
decodedReactions.set(reaction, decodedReaction);
} }
let emoji = legacies.get(decodedReaction.reaction); return Object.fromEntries(result);
if (emoji) {
_reactions.set(emoji, (_reactions.get(emoji) || 0) + reactions[reaction]);
} else {
_reactions.set(
reaction,
(_reactions.get(reaction) || 0) + reactions[reaction],
);
}
}
const _reactions2 = new Map();
for (const [reaction, count] of _reactions) {
const decodedReaction = decodedReactions.get(reaction);
_reactions2.set(decodedReaction.reaction, count);
}
return Object.fromEntries(_reactions2);
} }
export async function toDbReaction( export async function toDbReaction(
@ -66,9 +30,7 @@ export async function toDbReaction(
reacterHost = toPunyNullable(reacterHost); reacterHost = toPunyNullable(reacterHost);
// Convert string-type reactions to unicode if (reaction === "♥️") return "❤️";
const emoji = legacies.get(reaction) || (reaction === "♥️" ? "❤️" : null);
if (emoji) return emoji;
// Allow unicode reactions // Allow unicode reactions
const match = emojiRegex.exec(reaction); const match = emojiRegex.exec(reaction);
@ -128,9 +90,3 @@ export function decodeReaction(str: string): DecodedReaction {
host: undefined, host: undefined,
}; };
} }
export function convertLegacyReaction(reaction: string): string {
const decoded = decodeReaction(reaction).reaction;
if (legacies.has(decoded)) return legacies.get(decoded)!;
return decoded;
}

View file

@ -66,6 +66,12 @@ export class Note {
}) })
public text: string | null; public text: string | null;
@Column("varchar", {
length: 10,
nullable: true,
})
public lang: string | null;
@Column("varchar", { @Column("varchar", {
length: 256, length: 256,
nullable: true, nullable: true,

View file

@ -2,7 +2,7 @@ import { db } from "@/db/postgre.js";
import { NoteReaction } from "@/models/entities/note-reaction.js"; import { NoteReaction } from "@/models/entities/note-reaction.js";
import { Notes, Users } from "../index.js"; import { Notes, Users } from "../index.js";
import type { Packed } from "@/misc/schema.js"; import type { Packed } from "@/misc/schema.js";
import { convertLegacyReaction } from "@/misc/reaction-lib.js"; import { decodeReaction } from "@/misc/reaction-lib.js";
import type { User } from "@/models/entities/user.js"; import type { User } from "@/models/entities/user.js";
export const NoteReactionRepository = db.getRepository(NoteReaction).extend({ export const NoteReactionRepository = db.getRepository(NoteReaction).extend({
@ -27,7 +27,7 @@ export const NoteReactionRepository = db.getRepository(NoteReaction).extend({
id: reaction.id, id: reaction.id,
createdAt: reaction.createdAt.toISOString(), createdAt: reaction.createdAt.toISOString(),
user: await Users.pack(reaction.user ?? reaction.userId, me), user: await Users.pack(reaction.user ?? reaction.userId, me),
type: convertLegacyReaction(reaction.reaction), type: decodeReaction(reaction.reaction).reaction,
...(opts.withNote ...(opts.withNote
? { ? {
// may throw error // may throw error
@ -41,7 +41,7 @@ export const NoteReactionRepository = db.getRepository(NoteReaction).extend({
src: NoteReaction[], src: NoteReaction[],
me?: { id: User["id"] } | null | undefined, me?: { id: User["id"] } | null | undefined,
options?: { options?: {
withNote: booleam; withNote: boolean;
}, },
): Promise<Packed<"NoteReaction">[]> { ): Promise<Packed<"NoteReaction">[]> {
const reactions = await Promise.allSettled( const reactions = await Promise.allSettled(

View file

@ -14,11 +14,7 @@ import {
import type { Packed } from "@/misc/schema.js"; import type { Packed } from "@/misc/schema.js";
import { nyaize } from "@/misc/nyaize.js"; import { nyaize } from "@/misc/nyaize.js";
import { awaitAll } from "@/prelude/await-all.js"; import { awaitAll } from "@/prelude/await-all.js";
import { import { convertReactions, decodeReaction } from "@/misc/reaction-lib.js";
convertLegacyReaction,
convertLegacyReactions,
decodeReaction,
} from "@/misc/reaction-lib.js";
import type { NoteReaction } from "@/models/entities/note-reaction.js"; import type { NoteReaction } from "@/models/entities/note-reaction.js";
import { import {
aggregateNoteEmojis, aggregateNoteEmojis,
@ -27,7 +23,7 @@ import {
} from "@/misc/populate-emojis.js"; } from "@/misc/populate-emojis.js";
import { db } from "@/db/postgre.js"; import { db } from "@/db/postgre.js";
import { IdentifiableError } from "@/misc/identifiable-error.js"; import { IdentifiableError } from "@/misc/identifiable-error.js";
import { detect as detectLanguage_ } from "tinyld"; import { detect as detectLanguage } from "tinyld";
export async function populatePoll(note: Note, meId: User["id"] | null) { export async function populatePoll(note: Note, meId: User["id"] | null) {
const poll = await Polls.findOneByOrFail({ noteId: note.id }); const poll = await Polls.findOneByOrFail({ noteId: note.id });
@ -77,7 +73,7 @@ async function populateMyReaction(
if (_hint_?.myReactions) { if (_hint_?.myReactions) {
const reaction = _hint_.myReactions.get(note.id); const reaction = _hint_.myReactions.get(note.id);
if (reaction) { if (reaction) {
return convertLegacyReaction(reaction.reaction); return decodeReaction(reaction.reaction).reaction;
} else if (reaction === null) { } else if (reaction === null) {
return undefined; return undefined;
} }
@ -90,7 +86,7 @@ async function populateMyReaction(
}); });
if (reaction) { if (reaction) {
return convertLegacyReaction(reaction.reaction); return decodeReaction(reaction.reaction).reaction;
} }
return undefined; return undefined;
@ -203,8 +199,6 @@ export const NoteRepository = db.getRepository(Note).extend({
host, host,
); );
const lang =
detectLanguage_(`${note.cw ?? ""}\n${note.text ?? ""}`) ?? "unknown";
const reactionEmoji = await populateEmojis(reactionEmojiNames, host); const reactionEmoji = await populateEmojis(reactionEmojiNames, host);
const packed: Packed<"Note"> = await awaitAll({ const packed: Packed<"Note"> = await awaitAll({
id: note.id, id: note.id,
@ -221,7 +215,7 @@ export const NoteRepository = db.getRepository(Note).extend({
note.visibility === "specified" ? note.visibleUserIds : undefined, note.visibility === "specified" ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount, renoteCount: note.renoteCount,
repliesCount: note.repliesCount, repliesCount: note.repliesCount,
reactions: convertLegacyReactions(note.reactions), reactions: convertReactions(note.reactions),
reactionEmojis: reactionEmoji, reactionEmojis: reactionEmoji,
emojis: noteEmoji, emojis: noteEmoji,
tags: note.tags.length > 0 ? note.tags : undefined, tags: note.tags.length > 0 ? note.tags : undefined,
@ -264,7 +258,7 @@ export const NoteRepository = db.getRepository(Note).extend({
: undefined, : undefined,
} }
: {}), : {}),
lang: lang, lang: note.lang,
}); });
if (packed.user.isCat && packed.user.speakAsCat && packed.text) { if (packed.user.isCat && packed.user.speakAsCat && packed.text) {

View file

@ -53,6 +53,7 @@ import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js";
import { truncate } from "@/misc/truncate.js"; import { truncate } from "@/misc/truncate.js";
import { type Size, getEmojiSize } from "@/misc/emoji-meta.js"; import { type Size, getEmojiSize } from "@/misc/emoji-meta.js";
import { fetchMeta } from "@/misc/fetch-meta.js"; import { fetchMeta } from "@/misc/fetch-meta.js";
import { langmap } from "@/misc/langmap.js";
const logger = apLogger; const logger = apLogger;
@ -247,7 +248,7 @@ export async function createNote(
// Quote // Quote
let quote: Note | undefined | null; let quote: Note | undefined | null;
if (note._misskey_quote || note.quoteUrl || note.quoteUri) { if (note.quoteUrl || note.quoteUri) {
const tryResolveNote = async ( const tryResolveNote = async (
uri: string, uri: string,
): Promise< ): Promise<
@ -284,7 +285,7 @@ export async function createNote(
}; };
const uris = unique( const uris = unique(
[note._misskey_quote, note.quoteUrl, note.quoteUri].filter( [note.quoteUrl, note.quoteUri].filter(
(x): x is string => typeof x === "string", (x): x is string => typeof x === "string",
), ),
); );
@ -305,13 +306,24 @@ export async function createNote(
// Text parsing // Text parsing
let text: string | null = null; let text: string | null = null;
let lang: string | null = null;
if ( if (
note.source?.mediaType === "text/x.misskeymarkdown" && note.source?.mediaType === "text/x.misskeymarkdown" &&
typeof note.source?.content === "string" typeof note.source?.content === "string"
) { ) {
text = note.source.content; text = note.source.content;
} else if (typeof note._misskey_content !== "undefined") { if (note.contentMap != null) {
text = note._misskey_content; const key = Object.keys(note.contentMap)[0];
lang = Object.keys(langmap).includes(key)
? key.trim().split("-")[0].split("@")[0]
: null;
}
} else if (note.contentMap != null) {
const entry = Object.entries(note.contentMap)[0];
lang = Object.keys(langmap).includes(entry[0])
? entry[0].trim().split("-")[0].split("@")[0]
: null;
text = htmlToMfm(entry[1], note.tag);
} else if (typeof note.content === "string") { } else if (typeof note.content === "string") {
text = htmlToMfm(note.content, note.tag); text = htmlToMfm(note.content, note.tag);
} }
@ -380,6 +392,7 @@ export async function createNote(
name: note.name, name: note.name,
cw, cw,
text, text,
lang,
localOnly: false, localOnly: false,
visibility, visibility,
visibleUsers, visibleUsers,
@ -567,13 +580,24 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
// Text parsing // Text parsing
let text: string | null = null; let text: string | null = null;
let lang: string | null = null;
if ( if (
post.source?.mediaType === "text/x.misskeymarkdown" && post.source?.mediaType === "text/x.misskeymarkdown" &&
typeof post.source?.content === "string" typeof post.source?.content === "string"
) { ) {
text = post.source.content; text = post.source.content;
} else if (typeof post._misskey_content !== "undefined") { if (post.contentMap != null) {
text = post._misskey_content; const key = Object.keys(post.contentMap)[0];
lang = Object.keys(langmap).includes(key)
? key.trim().split("-")[0].split("@")[0]
: null;
}
} else if (post.contentMap != null) {
const entry = Object.entries(post.contentMap)[0];
lang = Object.keys(langmap).includes(entry[0])
? entry[0].trim().split("-")[0].split("@")[0]
: null;
text = htmlToMfm(entry[1], post.tag);
} else if (typeof post.content === "string") { } else if (typeof post.content === "string") {
text = htmlToMfm(post.content, post.tag); text = htmlToMfm(post.content, post.tag);
} }
@ -667,6 +691,9 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
if (text && text !== note.text) { if (text && text !== note.text) {
update.text = text; update.text = text;
} }
if (lang && lang !== note.lang) {
update.lang = lang;
}
if (cw !== note.cw) { if (cw !== note.cw) {
update.cw = cw ? cw : null; update.cw = cw ? cw : null;
} }

View file

@ -40,11 +40,9 @@ export const renderActivity = (x: any): IActivity | null => {
speakAsCat: "firefish:speakAsCat", speakAsCat: "firefish:speakAsCat",
// Misskey // Misskey
misskey: "https://misskey-hub.net/ns#", misskey: "https://misskey-hub.net/ns#",
_misskey_content: "misskey:_misskey_content", _misskey_talk: "misskey:_misskey_talk",
_misskey_quote: "misskey:_misskey_quote",
_misskey_reaction: "misskey:_misskey_reaction", _misskey_reaction: "misskey:_misskey_reaction",
_misskey_votes: "misskey:_misskey_votes", _misskey_votes: "misskey:_misskey_votes",
_misskey_talk: "misskey:_misskey_talk",
isCat: "misskey:isCat", isCat: "misskey:isCat",
// Fedibird // Fedibird
fedibird: "http://fedibird.com/ns#", fedibird: "http://fedibird.com/ns#",

View file

@ -1,4 +1,5 @@
import { In, IsNull } from "typeorm"; import { In, IsNull } from "typeorm";
import { detect as detectLanguage } from "tinyld";
import config from "@/config/index.js"; import config from "@/config/index.js";
import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js"; import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js";
import type { DriveFile } from "@/models/entities/drive-file.js"; import type { DriveFile } from "@/models/entities/drive-file.js";
@ -114,6 +115,13 @@ export default async function renderNote(
}), }),
); );
const lang = note.lang ?? detectLanguage(text);
const contentMap = lang
? {
[lang]: content,
}
: null;
const emojis = await getEmojis(note.emojis); const emojis = await getEmojis(note.emojis);
const apemojis = emojis.map((emoji) => renderEmoji(emoji)); const apemojis = emojis.map((emoji) => renderEmoji(emoji));
@ -152,12 +160,11 @@ export default async function renderNote(
attributedTo, attributedTo,
summary, summary,
content, content,
_misskey_content: text, contentMap,
source: { source: {
content: text, content: text,
mediaType: "text/x.misskeymarkdown", mediaType: "text/x.misskeymarkdown",
}, },
_misskey_quote: quote,
quoteUri: quote, quoteUri: quote,
quoteUrl: quote, quoteUrl: quote,
published: note.createdAt.toISOString(), published: note.createdAt.toISOString(),

View file

@ -14,6 +14,7 @@ export interface IObject {
inReplyTo?: any; inReplyTo?: any;
replies?: ICollection; replies?: ICollection;
content?: string; content?: string;
contentMap?: obj;
name?: string; name?: string;
startTime?: Date; startTime?: Date;
endTime?: Date; endTime?: Date;
@ -134,7 +135,6 @@ export interface IPost extends IObject {
content: string; content: string;
mediaType: string; mediaType: string;
}; };
_misskey_quote?: string;
quoteUrl?: string; quoteUrl?: string;
quoteUri?: string; quoteUri?: string;
_misskey_talk: boolean; _misskey_talk: boolean;
@ -146,7 +146,6 @@ export interface IQuestion extends IObject {
content: string; content: string;
mediaType: string; mediaType: string;
}; };
_misskey_quote?: string;
quoteUrl?: string; quoteUrl?: string;
oneOf?: IQuestionChoice[]; oneOf?: IQuestionChoice[];
anyOf?: IQuestionChoice[]; anyOf?: IQuestionChoice[];

View file

@ -108,6 +108,7 @@ export const paramDef = {
}, },
}, },
text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true },
lang: { type: "string", nullable: true, maxLength: 10 },
cw: { type: "string", nullable: true, maxLength: 100 }, cw: { type: "string", nullable: true, maxLength: 100 },
localOnly: { type: "boolean", default: false }, localOnly: { type: "boolean", default: false },
noExtractMentions: { type: "boolean", default: false }, noExtractMentions: { type: "boolean", default: false },
@ -294,6 +295,7 @@ export default define(meta, paramDef, async (ps, user) => {
} }
: undefined, : undefined,
text: ps.text || undefined, text: ps.text || undefined,
lang: ps.lang,
reply, reply,
renote, renote,
cw: ps.cw, cw: ps.cw,

View file

@ -35,6 +35,8 @@ import renderUpdate from "@/remote/activitypub/renderer/update.js";
import { deliverToRelays } from "@/services/relay.js"; import { deliverToRelays } from "@/services/relay.js";
// import { deliverQuestionUpdate } from "@/services/note/polls/update.js"; // import { deliverQuestionUpdate } from "@/services/note/polls/update.js";
import { fetchMeta } from "@/misc/fetch-meta.js"; import { fetchMeta } from "@/misc/fetch-meta.js";
import { detect as detectLanguage } from "tinyld";
import { langmap } from "@/misc/langmap.js";
export const meta = { export const meta = {
tags: ["notes"], tags: ["notes"],
@ -169,6 +171,7 @@ export const paramDef = {
}, },
}, },
text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true },
lang: { type: "string", nullable: true, maxLength: 10 },
cw: { type: "string", nullable: true, maxLength: 250 }, cw: { type: "string", nullable: true, maxLength: 250 },
localOnly: { type: "boolean", default: false }, localOnly: { type: "boolean", default: false },
noExtractMentions: { type: "boolean", default: false }, noExtractMentions: { type: "boolean", default: false },
@ -375,6 +378,16 @@ export default define(meta, paramDef, async (ps, user) => {
ps.text = null; ps.text = null;
} }
if (ps.lang) {
if (!Object.keys(langmap).includes(ps.lang.trim()))
throw new Error("invalid param");
ps.lang = ps.lang.trim().split("-")[0].split("@")[0];
} else if (ps.text) {
ps.lang = detectLanguage(ps.text);
} else {
ps.lang = null;
}
let tags = []; let tags = [];
let emojis = []; let emojis = [];
let mentionedUsers = []; let mentionedUsers = [];
@ -532,6 +545,9 @@ export default define(meta, paramDef, async (ps, user) => {
if (ps.text !== note.text) { if (ps.text !== note.text) {
update.text = ps.text; update.text = ps.text;
} }
if (ps.lang !== note.lang) {
update.lang = ps.lang;
}
if (ps.cw !== note.cw || (ps.cw && !note.cw)) { if (ps.cw !== note.cw || (ps.cw && !note.cw)) {
update.cw = ps.cw; update.cw = ps.cw;
} }

View file

@ -492,7 +492,7 @@ router.get("/notes/:note", async (ctx, next) => {
ctx.set("Cache-Control", "public, max-age=15"); ctx.set("Cache-Control", "public, max-age=15");
ctx.set( ctx.set(
"Content-Security-Policy", "Content-Security-Policy",
"default-src 'self' 'unsafe-inline'; img-src *; frame-ancestors *", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; font-src 'self' data:; img-src *; media-src *; worker-src 'self'; frame-ancestors *",
); );
return; return;

View file

@ -61,6 +61,8 @@ import { shouldSilenceInstance } from "@/misc/should-block-instance.js";
import meilisearch from "../../db/meilisearch.js"; import meilisearch from "../../db/meilisearch.js";
import { redisClient } from "@/db/redis.js"; import { redisClient } from "@/db/redis.js";
import { Mutex } from "redis-semaphore"; import { Mutex } from "redis-semaphore";
import { detect as detectLanguage } from "tinyld";
import { langmap } from "@/misc/langmap.js";
const mutedWordsCache = new Cache< const mutedWordsCache = new Cache<
{ userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[]
@ -133,6 +135,7 @@ type Option = {
createdAt?: Date | null; createdAt?: Date | null;
name?: string | null; name?: string | null;
text?: string | null; text?: string | null;
lang?: string | null;
reply?: Note | null; reply?: Note | null;
renote?: Note | null; renote?: Note | null;
files?: DriveFile[] | null; files?: DriveFile[] | null;
@ -270,6 +273,16 @@ export default async (
data.text = null; data.text = null;
} }
if (data.lang) {
if (!Object.keys(langmap).includes(data.lang.trim()))
throw new Error("invalid param");
data.lang = data.lang.trim().split("-")[0].split("@")[0];
} else if (data.text) {
data.lang = detectLanguage(data.text);
} else {
data.lang = null;
}
let tags = data.apHashtags; let tags = data.apHashtags;
let emojis = data.apEmojis; let emojis = data.apEmojis;
let mentionedUsers = data.apMentions; let mentionedUsers = data.apMentions;
@ -699,6 +712,7 @@ async function insertNote(
: null, : null,
name: data.name, name: data.name,
text: data.text, text: data.text,
lang: data.lang,
hasPoll: data.poll != null, hasPoll: data.poll != null,
cw: data.cw == null ? null : data.cw, cw: data.cw == null ? null : data.cw,
tags: tags.map((tag) => normalizeForSearch(tag)), tags: tags.map((tag) => normalizeForSearch(tag)),

View file

@ -60,6 +60,7 @@
"insert-text-at-cursor": "0.3.0", "insert-text-at-cursor": "0.3.0",
"json5": "2.2.3", "json5": "2.2.3",
"katex": "0.16.8", "katex": "0.16.8",
"libopenmpt-wasm": "github:TheEssem/libopenmpt-packaging#build",
"matter-js": "0.19.0", "matter-js": "0.19.0",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"photoswipe": "5.3.9", "photoswipe": "5.3.9",

View file

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

View file

@ -12,16 +12,28 @@
:class="{ dmWidth: inDm }" :class="{ dmWidth: inDm }"
> >
<div ref="gallery" @click.stop> <div ref="gallery" @click.stop>
<XMedia <template
v-for="media in mediaList.filter((media) => v-for="media in mediaList.filter((media) =>
previewable(media), previewable(media),
)" )"
>
<XMedia
v-if="
media.type.startsWith('video') ||
media.type.startsWith('image')
"
:key="media.id" :key="media.id"
:class="{ image: media.type.startsWith('image') }" :class="{ image: media.type.startsWith('image') }"
:data-id="media.id" :data-id="media.id"
:media="media" :media="media"
:raw="raw" :raw="raw"
/> />
<XModPlayer
v-else-if="isModule(media)"
:key="media.id"
:module="media"
/>
</template>
</div> </div>
</div> </div>
</div> </div>
@ -35,8 +47,13 @@ import PhotoSwipe from "photoswipe";
import "photoswipe/style.css"; import "photoswipe/style.css";
import XBanner from "@/components/MkMediaBanner.vue"; import XBanner from "@/components/MkMediaBanner.vue";
import XMedia from "@/components/MkMedia.vue"; import XMedia from "@/components/MkMedia.vue";
import XModPlayer from "@/components/MkModPlayer.vue";
import * as os from "@/os"; import * as os from "@/os";
import { FILE_TYPE_BROWSERSAFE } from "@/const"; import {
FILE_TYPE_BROWSERSAFE,
FILE_TYPE_TRACKER_MODULES,
FILE_EXT_TRACKER_MODULES,
} from "@/const";
const props = defineProps<{ const props = defineProps<{
mediaList: misskey.entities.DriveFile[]; mediaList: misskey.entities.DriveFile[];
@ -170,11 +187,24 @@ onMounted(() => {
const previewable = (file: misskey.entities.DriveFile): boolean => { const previewable = (file: misskey.entities.DriveFile): boolean => {
if (file.type === "image/svg+xml") return true; // svgwebpublic/thumbnailpngtrue if (file.type === "image/svg+xml") return true; // svgwebpublic/thumbnailpngtrue
// FILE_TYPE_BROWSERSAFE // FILE_TYPE_BROWSERSAFE
if (isModule(file)) return true;
return ( return (
(file.type.startsWith("video") || file.type.startsWith("image")) && (file.type.startsWith("video") || file.type.startsWith("image")) &&
FILE_TYPE_BROWSERSAFE.includes(file.type) FILE_TYPE_BROWSERSAFE.includes(file.type)
); );
}; };
const isModule = (file: misskey.entities.DriveFile): boolean => {
return (
FILE_TYPE_TRACKER_MODULES.some((type) => {
return file.type === type;
}) ||
FILE_EXT_TRACKER_MODULES.some((ext) => {
return file.name.toLowerCase().endsWith("." + ext);
})
);
};
const previewableCount = props.mediaList.filter((media) => const previewableCount = props.mediaList.filter((media) =>
previewable(media), previewable(media),
).length; ).length;

View file

@ -0,0 +1,516 @@
<template>
<div class="mod-player-disabled" v-if="!available">
<MkLoading v-if="fetching" />
<MkError v-else-if="error" @retry="load()" />
</div>
<div class="mod-player-disabled" v-else-if="hide" @click="toggleVisible()">
<div>
<b
><i class="ph-warning ph-bold ph-lg"></i>
{{ i18n.ts.sensitive }}</b
>
<span>{{ i18n.ts.clickToShow }}</span>
</div>
</div>
<div class="mod-player-enabled" v-else>
<div class="pattern-display">
<div class="mod-pattern" ref="modPattern" v-if="patternShow">
<span
v-for="(row, i) in patData[currentPattern]"
ref="initRow"
v-bind:class="{ modRowActive: isRowActive(i) }"
v-if="patData.length !== 0"
>
<span v-bind:class="{ modColQuarter: i % 4 === 0 }">{{
indexText(i)
}}</span>
<span class="mod-row-inner">{{ getRowText(row) }}</span>
</span>
<MkLoading v-else />
</div>
<div class="mod-pattern" v-else @click="showPattern()">
<span class="modRowActive" ref="initRow">
<span class="modColQuarter">00</span>
<span class="mod-row-inner">|F-12Ev10XEF</span>
</span>
<br />
<p>{{ i18n.ts.clickToShowPatterns }}</p>
</div>
</div>
<div class="controls">
<button class="play" @click="playPause()" v-if="!loading">
<i class="ph-pause ph-fill ph-lg" v-if="playing"></i>
<i class="ph-play ph-fill ph-lg" v-else></i>
</button>
<MkLoading v-else :em="true" />
<button class="stop" @click="stop()">
<i class="ph-stop ph-fill ph-lg"></i>
</button>
<button class="loop" @click="toggleLoop()">
<i class="ph-repeat ph-fill ph-lg" v-if="loop === -1"></i>
<i class="ph-repeat-once ph-fill ph-lg" v-else></i>
</button>
<FormRange
class="progress"
:min="0"
:max="length"
v-model="position"
:step="0.1"
ref="progress"
:background="false"
:tooltips="false"
:instant="true"
@update:modelValue="performSeek()"
></FormRange>
<button class="mute" @click="toggleMute()">
<i class="ph-speaker-simple-x ph-fill ph-lg" v-if="muted"></i>
<i class="ph-speaker-simple-high ph-fill ph-lg" v-else></i>
</button>
<FormRange
class="volume"
:min="0"
:max="1"
v-model="player.context.gain.value"
:step="0.1"
:background="false"
:tooltips="false"
:instant="true"
@update:modelValue="updateMute()"
></FormRange>
<a
class="download"
:title="i18n.ts.download"
:href="module.url"
target="_blank"
>
<i class="ph-download-simple ph-fill ph-lg"></i>
</a>
</div>
<div class="buttons">
<button
v-if="module.comment"
v-tooltip="i18n.ts.alt"
class="_button"
@click.stop="captionPopup"
>
<i class="ph-subtitles ph-bold ph-lg"></i>
</button>
<button
v-if="!hide"
v-tooltip="i18n.ts.hide"
class="_button"
@click.stop="toggleVisible()"
>
<i class="ph-eye-slash ph-bold ph-lg"></i>
</button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, shallowRef, nextTick, onDeactivated, onMounted } from "vue";
import * as firefish from "firefish-js";
import FormRange from "./form/range.vue";
import { i18n } from "@/i18n";
import * as os from "@/os";
import { defaultStore } from "@/store";
import { ChiptuneJsPlayer, ChiptuneJsConfig } from "@/scripts/chiptune2";
const props = defineProps<{
module: firefish.entities.DriveFile;
}>();
interface ModRow {
notes: string[];
insts: string[];
vols: string[];
fxs: string[];
ops: string[];
}
const available = ref(false);
const initRow = shallowRef<HTMLSpanElement>();
const player = shallowRef(new ChiptuneJsPlayer(new ChiptuneJsConfig()));
let hide = ref(
defaultStore.state.nsfw === "force"
? true
: props.module.isSensitive && defaultStore.state.nsfw !== "ignore",
);
let playing = ref(false);
let patternShow = ref(false);
let modPattern = ref<HTMLDivElement>();
let progress = ref<typeof FormRange>();
let position = ref(0);
let patData = shallowRef([] as ModRow[][]);
let currentPattern = ref(0);
let nbChannels = ref(0);
let length = ref(1);
let muted = ref(false);
let loop = ref(0);
let fetching = ref(true);
let error = ref(false);
let loading = ref(false);
function load() {
player.value
.load(props.module.url)
.then((result: null) => {
buffer = result;
available.value = true;
error.value = false;
fetching.value = false;
})
.catch((e: any) => {
console.error(e);
error.value = true;
fetching.value = false;
});
}
onMounted(load);
let currentRow = 0;
let rowHeight = 0;
let buffer = null;
let isSeeking = false;
function captionPopup() {
os.alert({
type: "info",
text: props.module.comment,
});
}
function showPattern() {
patternShow.value = !patternShow.value;
nextTick(() => {
if (playing.value) display();
else stop();
});
}
function getRowText(row: ModRow) {
let text = "";
for (let i = 0; i < row.notes.length; i++) {
text = text.concat(
"|",
row.notes[i],
row.insts[i],
row.vols[i],
row.fxs[i],
row.ops[i],
);
}
return text;
}
function playPause() {
player.value.addHandler("onRowChange", (i: { index: number }) => {
currentRow = i.index;
currentPattern.value = player.value.getPattern();
length.value = player.value.duration();
if (!isSeeking) {
position.value = player.value.position();
}
requestAnimationFrame(display);
});
player.value.addHandler("onEnded", () => {
stop();
});
if (player.value.currentPlayingNode === null) {
loading.value = true;
player.value.play(buffer).then(() => {
player.value.seek(position.value);
player.value.repeat(loop.value);
playing.value = true;
loading.value = false;
});
} else {
player.value.togglePause();
playing.value = !player.value.currentPlayingNode.paused;
}
}
async function stop(noDisplayUpdate = false) {
player.value.stop();
playing.value = false;
if (!noDisplayUpdate) {
try {
await player.value.play(buffer);
display(0, true);
} catch (e) {
console.warn(e);
}
}
player.value.stop();
position.value = 0;
currentRow = 0;
player.value.clearHandlers();
}
function toggleLoop() {
loop.value = loop.value === -1 ? 0 : -1;
player.value.repeat(loop.value);
}
let savedVolume = 0;
function toggleMute() {
if (muted.value) {
player.value.context.gain.value = savedVolume;
savedVolume = 0;
} else {
savedVolume = player.value.context.gain.value;
player.value.context.gain.value = 0;
}
muted.value = !muted.value;
}
function updateMute() {
muted.value = false;
savedVolume = 0;
}
function performSeek() {
player.value.seek(position.value);
display();
}
function toggleVisible() {
hide.value = !hide.value;
nextTick(() => {
stop(hide.value);
});
}
function isRowActive(i: number) {
if (i === currentRow) {
if (modPattern.value) {
if (rowHeight === 0 && initRow.value)
rowHeight = initRow.value[0].getBoundingClientRect().height;
modPattern.value.scrollTop = currentRow * rowHeight;
}
return true;
}
return;
}
function indexText(i: number) {
let rowText = i.toString(16);
if (rowText.length === 1) {
rowText = "0" + rowText;
}
return rowText;
}
function getRow(pattern: number, rowOffset: number) {
let notes: string[] = [],
insts: string[] = [],
vols: string[] = [],
fxs: string[] = [],
ops: string[] = [];
for (let channel = 0; channel < nbChannels.value; channel++) {
const part = player.value.getPatternRowChannel(
pattern,
rowOffset,
channel,
);
notes.push(part.substring(0, 3));
insts.push(part.substring(4, 6));
vols.push(part.substring(6, 9));
fxs.push(part.substring(10, 11));
ops.push(part.substring(11, 13));
}
return {
notes,
insts,
vols,
fxs,
ops,
};
}
function display(_time = 0, reset = false) {
if (!patternShow.value) return;
if (reset) {
const pattern = player.value.getPattern();
currentPattern.value = pattern;
}
if (patData.value.length === 0) {
const nbPatterns = player.value.getNumPatterns();
const pattern = player.value.getPattern();
currentPattern.value = pattern;
if (player.value.currentPlayingNode) {
nbChannels.value = player.value.currentPlayingNode.nbChannels;
}
const patternsArray: ModRow[][] = [];
for (let patOffset = 0; patOffset < nbPatterns; patOffset++) {
const rowsArray: ModRow[] = [];
const nbRows = player.value.getPatternNumRows(patOffset);
for (let rowOffset = 0; rowOffset < nbRows; rowOffset++) {
rowsArray.push(getRow(patOffset, rowOffset));
}
patternsArray.push(rowsArray);
}
patData.value = Object.freeze(patternsArray);
}
}
onDeactivated(() => {
stop();
});
</script>
<style lang="scss" scoped>
.mod-player-enabled {
position: relative;
display: flex;
flex-direction: column;
> i {
display: block;
position: absolute;
border-radius: 6px;
background-color: var(--fg);
color: var(--accentLighten);
font-size: 14px;
opacity: 0.5;
padding: 3px 6px;
text-align: center;
cursor: pointer;
top: 12px;
right: 12px;
}
> .buttons {
display: flex;
gap: 4px;
position: absolute;
border-radius: 6px;
overflow: hidden;
top: 12px;
right: 12px;
> * {
background-color: var(--accentedBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: var(--accent);
font-size: 0.8em;
padding: 6px 8px;
text-align: center;
}
}
> .pattern-display {
width: 100%;
height: 100%;
overflow: hidden;
color: #ffffff;
background-color: black;
text-align: center;
font: 12px monospace;
white-space: pre;
user-select: none;
> .mod-pattern {
display: grid;
overflow-y: hidden;
height: 0;
padding-top: calc((56.25% - 48px) / 2);
padding-bottom: calc((56.25% - 48px) / 2);
content-visibility: auto;
> .modRowActive {
opacity: 1;
}
> span {
opacity: 0.5;
> .modColQuarter {
color: #ffff00;
}
> .mod-row-inner {
background: repeating-linear-gradient(
to right,
white 0 4ch,
#80e0ff 4ch 6ch,
#80ff80 6ch 9ch,
#ff80e0 9ch 10ch,
#ffe080 10ch 12ch
);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
}
}
> .controls {
display: flex;
width: 100%;
background-color: var(--panelHighlight);
> * {
padding: 4px 8px;
}
> button,
a {
border: none;
background-color: transparent;
color: var(--navFg);
cursor: pointer;
margin: auto;
&:hover {
background-color: var(--accentedBg);
border-radius: 3px;
}
}
> .progress {
flex-grow: 1;
min-width: 0;
}
> .volume {
flex-shrink: 1;
max-width: 128px;
}
}
}
.mod-player-disabled {
display: flex;
justify-content: center;
align-items: center;
background: #111;
color: #fff;
> div {
display: table-cell;
text-align: center;
font-size: 12px;
> b {
display: block;
}
}
}
</style>

View file

@ -97,7 +97,11 @@
<div class="main"> <div class="main">
<div class="header-container"> <div class="header-container">
<MkAvatar class="avatar" :user="appearNote.user" /> <MkAvatar class="avatar" :user="appearNote.user" />
<XNoteHeader class="header" :note="appearNote" /> <XNoteHeader
class="header"
:note="appearNote"
:can-open-server-info="true"
/>
</div> </div>
<div class="body"> <div class="body">
<MkSubNoteContent <MkSubNoteContent

View file

@ -41,6 +41,7 @@
class="ticker" class="ticker"
:instance="note.user.instance" :instance="note.user.instance"
:host="note.user.host" :host="note.user.host"
@click.stop="openServerInfo"
/> />
</div> </div>
</div> </div>
@ -57,10 +58,12 @@ import MkInstanceTicker from "@/components/MkInstanceTicker.vue";
import { notePage } from "@/filters/note"; import { notePage } from "@/filters/note";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { pageWindow } from "@/os";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
pinned?: boolean; pinned?: boolean;
canOpenServerInfo?: boolean;
}>(); }>();
const note = ref(props.note); const note = ref(props.note);
@ -69,6 +72,19 @@ const showTicker =
defaultStore.state.instanceTicker === "always" || defaultStore.state.instanceTicker === "always" ||
(defaultStore.state.instanceTicker === "remote" && (defaultStore.state.instanceTicker === "remote" &&
note.value.user.instance); note.value.user.instance);
function openServerInfo() {
if (
(props.canOpenServerInfo && !defaultStore.state.openServerInfo) ||
!note.value.user.instance
)
return;
const instanceInfoUrl =
props.host == null
? "/about"
: `/instance-info/${note.value.user.instance}`;
pageWindow(instanceInfoUrl);
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,7 +1,7 @@
<template> <template>
<label class="timctyfi" :class="{ disabled, easing }"> <label class="timctyfi" :class="{ disabled, easing }">
<div class="label"><slot name="label"></slot></div> <div class="label"><slot name="label"></slot></div>
<div v-adaptive-border class="body"> <div v-adaptive-border class="body" :class="{ background }">
<div class="container"> <div class="container">
<input <input
ref="inputEl" ref="inputEl"
@ -19,7 +19,12 @@
@touchend="tooltipHide" @touchend="tooltipHide"
@mouseenter="tooltipShow" @mouseenter="tooltipShow"
@mouseleave="tooltipHide" @mouseleave="tooltipHide"
@input="(x) => (inputVal = x.target.value)" @input="
(x) => {
inputVal = x.target.value;
if (instant) onChange(x);
}
"
/> />
<datalist v-if="showTicks && steps" :id="id"> <datalist v-if="showTicks && steps" :id="id">
<option <option
@ -50,11 +55,17 @@ const props = withDefaults(
textConverter?: (value: number) => string; textConverter?: (value: number) => string;
showTicks?: boolean; showTicks?: boolean;
easing?: boolean; easing?: boolean;
background?: boolean;
tooltips?: boolean;
instant?: boolean;
}>(), }>(),
{ {
step: 1, step: 1,
textConverter: (v) => v.toString(), textConverter: (v) => v.toString(),
easing: false, easing: false,
background: true,
tooltips: true,
instant: false,
}, },
); );
@ -79,6 +90,7 @@ function onChange(x) {
const tooltipShowing = ref(false); const tooltipShowing = ref(false);
function tooltipShow() { function tooltipShow() {
if (!props.tooltips) return;
tooltipShowing.value = true; tooltipShowing.value = true;
os.popup( os.popup(
defineAsyncComponent(() => import("@/components/MkTooltip.vue")), defineAsyncComponent(() => import("@/components/MkTooltip.vue")),
@ -94,6 +106,7 @@ function tooltipShow() {
); );
} }
function tooltipHide() { function tooltipHide() {
if (!props.tooltips) return;
tooltipShowing.value = false; tooltipShowing.value = false;
} }
</script> </script>
@ -128,13 +141,21 @@ function tooltipHide() {
$thumbWidth: 20px; $thumbWidth: 20px;
> .body { > .body {
padding: 10px 0;
background: none;
border: none;
border-radius: 6px;
&.background {
padding: 10px 12px; padding: 10px 12px;
background: var(--panel); background: var(--panel);
border: solid 1px var(--panel); border: solid 1px var(--panel);
border-radius: 6px; }
> .container { > .container {
position: relative; position: relative;
display: flex;
align-items: center;
height: $thumbHeight; height: $thumbHeight;
@mixin track { @mixin track {
@ -155,6 +176,7 @@ function tooltipHide() {
&:hover { &:hover {
background: var(--accentLighten); background: var(--accentLighten);
cursor: pointer;
} }
} }
> input { > input {

View file

@ -98,7 +98,7 @@ watch(
<style lang="scss" scoped> <style lang="scss" scoped>
@keyframes earwiggleleft { @keyframes earwiggleleft {
from { 0% {
transform: rotate(37.6deg) skew(30deg); transform: rotate(37.6deg) skew(30deg);
} }
25% { 25% {
@ -110,13 +110,13 @@ watch(
75% { 75% {
transform: rotate(0deg) skew(30deg); transform: rotate(0deg) skew(30deg);
} }
to { 100% {
transform: rotate(37.6deg) skew(30deg); transform: rotate(37.6deg) skew(30deg);
} }
} }
@keyframes earwiggleright { @keyframes earwiggleright {
from { 0% {
transform: rotate(-37.6deg) skew(-30deg); transform: rotate(-37.6deg) skew(-30deg);
} }
30% { 30% {
@ -128,7 +128,7 @@ watch(
75% { 75% {
transform: rotate(0deg) skew(-30deg); transform: rotate(0deg) skew(-30deg);
} }
to { 100% {
transform: rotate(-37.6deg) skew(-30deg); transform: rotate(-37.6deg) skew(-30deg);
} }
} }

View file

@ -297,7 +297,7 @@ const props = withDefaults(
} }
@keyframes mfm-rubberBand { @keyframes mfm-rubberBand {
from { 0% {
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
} }
30% { 30% {
@ -315,7 +315,7 @@ const props = withDefaults(
75% { 75% {
transform: scale3d(1.05, 0.95, 1); transform: scale3d(1.05, 0.95, 1);
} }
to { 100% {
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
} }
} }

View file

@ -38,6 +38,74 @@ export const FILE_TYPE_BROWSERSAFE = [
"audio/x-flac", "audio/x-flac",
"audio/vnd.wave", "audio/vnd.wave",
]; ];
export const FILE_TYPE_TRACKER_MODULES = [
"audio/mod",
"audio/x-mod",
"audio/s3m",
"audio/x-s3m",
"audio/xm",
"audio/x-xm",
"audio/it",
"audio/x-it",
];
export const FILE_EXT_TRACKER_MODULES = [
"mptm",
"mod",
"s3m",
"xm",
"it",
"667",
"669",
"amf",
"ams",
"c67",
"dbm",
"digi",
"dmf",
"dsm",
"dsym",
"dtm",
"far",
"fmt",
"imf",
"ice",
"j2b",
"m15",
"mdl",
"med",
"mms",
"mt2",
"mtm",
"mus",
"nst",
"okt",
"plm",
"psm",
"pt36",
"ptm",
"sfx",
"sfx2",
"st26",
"stk",
"stm",
"stx",
"stp",
"symmod",
"gtk",
"gt2",
"ult",
"wow",
"xmf",
"gdm",
"mo3",
"oxm",
"umx",
"xpk",
"ppm",
"mmcmp",
];
/* /*
https://github.com/sindresorhus/file-type/blob/main/supported.js https://github.com/sindresorhus/file-type/blob/main/supported.js
https://github.com/sindresorhus/file-type/blob/main/core.js https://github.com/sindresorhus/file-type/blob/main/core.js

View file

@ -7,23 +7,18 @@
<div class="shape2"></div> <div class="shape2"></div>
<img src="/client-assets/misskey.svg" class="misskey" /> <img src="/client-assets/misskey.svg" class="misskey" />
<div class="emojis"> <div class="emojis">
<MkEmoji :normal="true" :no-style="true" emoji="⭐" /> <MkEmoji
<MkEmoji :normal="true" :no-style="true" emoji="❤️" /> v-for="reaction in defaultReactions"
<MkEmoji :normal="true" :no-style="true" emoji="😆" /> :normal="true"
<MkEmoji :normal="true" :no-style="true" emoji="🤔" /> :no-style="true"
<MkEmoji :normal="true" :no-style="true" emoji="😮" /> :emoji="reaction"
<MkEmoji :normal="true" :no-style="true" emoji="🎉" /> />
<MkEmoji :normal="true" :no-style="true" emoji="💢" />
<MkEmoji :normal="true" :no-style="true" emoji="😥" />
<MkEmoji :normal="true" :no-style="true" emoji="😇" />
<MkEmoji :normal="true" :no-style="true" emoji="🥴" />
<MkEmoji :normal="true" :no-style="true" emoji="🍮" />
</div> </div>
<div class="main"> <div class="main">
<img <img
:src=" :src="
$instance.iconUrl || instance.iconUrl ||
$instance.faviconUrl || instance.faviconUrl ||
'/favicon.ico' '/favicon.ico'
" "
alt="" alt=""
@ -110,7 +105,9 @@ import MkButton from "@/components/MkButton.vue";
import MkFeaturedPhotos from "@/components/MkFeaturedPhotos.vue"; import MkFeaturedPhotos from "@/components/MkFeaturedPhotos.vue";
import { instanceName } from "@/config"; import { instanceName } from "@/config";
import * as os from "@/os"; import * as os from "@/os";
import { instance } from "@/instance";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultReactions } from "@/store";
const meta = ref(); const meta = ref();
const stats = ref(); const stats = ref();
@ -183,6 +180,15 @@ function showMenu(ev) {
os.pageWindow("/about-firefish"); os.pageWindow("/about-firefish");
}, },
}, },
instance.tosUrl
? {
text: i18n.ts.tos,
icon: "ph-scroll ph-bold ph-lg",
action: () => {
window.open(instance.tosUrl, "_blank");
},
}
: null,
], ],
ev.currentTarget ?? ev.target, ev.currentTarget ?? ev.target,
); );

View file

@ -0,0 +1,372 @@
import wasm from "libopenmpt-wasm";
const ChiptuneAudioContext = window.AudioContext;
export function ChiptuneJsConfig(repeatCount?: number, context?: AudioContext) {
this.repeatCount = repeatCount;
this.context = context;
}
ChiptuneJsConfig.prototype.constructor = ChiptuneJsConfig;
export function ChiptuneJsPlayer(config: object) {
this.libopenmpt = null;
this.config = config;
this.audioContext = config.context || new ChiptuneAudioContext();
this.context = this.audioContext.createGain();
this.currentPlayingNode = null;
this.handlers = [];
this.touchLocked = true;
this.volume = 1;
}
ChiptuneJsPlayer.prototype.constructor = ChiptuneJsPlayer;
ChiptuneJsPlayer.prototype.fireEvent = function (eventName: string, response) {
const handlers = this.handlers;
if (handlers.length > 0) {
for (const handler of handlers) {
if (handler.eventName === eventName) {
handler.handler(response);
}
}
}
};
ChiptuneJsPlayer.prototype.addHandler = function (
eventName: string,
handler: Function,
) {
this.handlers.push({ eventName, handler });
};
ChiptuneJsPlayer.prototype.clearHandlers = function () {
this.handlers = [];
};
ChiptuneJsPlayer.prototype.onEnded = function (handler: Function) {
this.addHandler("onEnded", handler);
};
ChiptuneJsPlayer.prototype.onError = function (handler: Function) {
this.addHandler("onError", handler);
};
ChiptuneJsPlayer.prototype.duration = function () {
return this.libopenmpt._openmpt_module_get_duration_seconds(
this.currentPlayingNode.modulePtr,
);
};
ChiptuneJsPlayer.prototype.position = function () {
return this.libopenmpt._openmpt_module_get_position_seconds(
this.currentPlayingNode.modulePtr,
);
};
ChiptuneJsPlayer.prototype.repeat = function (repeatCount: number) {
if (this.currentPlayingNode) {
this.libopenmpt._openmpt_module_set_repeat_count(
this.currentPlayingNode.modulePtr,
repeatCount,
);
}
};
ChiptuneJsPlayer.prototype.seek = function (position: number) {
if (this.currentPlayingNode) {
this.libopenmpt._openmpt_module_set_position_seconds(
this.currentPlayingNode.modulePtr,
position,
);
}
};
ChiptuneJsPlayer.prototype.metadata = function () {
const data = {};
const keys = this.libopenmpt
.UTF8ToString(
this.libopenmpt._openmpt_module_get_metadata_keys(
this.currentPlayingNode.modulePtr,
),
)
.split(";");
let keyNameBuffer = 0;
for (const key of keys) {
keyNameBuffer = this.libopenmpt._malloc(key.length + 1);
this.libopenmpt.stringToUTF8(key, keyNameBuffer);
data[key] = this.libopenmpt.UTF8ToString(
this.libopenmpt._openmpt_module_get_metadata(
this.currentPlayingNode.modulePtr,
keyNameBuffer,
),
);
this.libopenmpt._free(keyNameBuffer);
}
return data;
};
ChiptuneJsPlayer.prototype.unlock = function () {
const context = this.audioContext;
const buffer = context.createBuffer(1, 1, 22050);
const unlockSource = context.createBufferSource();
unlockSource.buffer = buffer;
unlockSource.connect(this.context);
this.context.connect(context.destination);
unlockSource.start(0);
this.touchLocked = false;
};
ChiptuneJsPlayer.prototype.load = function (input) {
return new Promise((resolve, reject) => {
if (this.touchLocked) {
this.unlock();
}
if (input instanceof File) {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsArrayBuffer(input);
} else {
window
.fetch(input)
.then((response) => {
response
.arrayBuffer()
.then((arrayBuffer) => {
resolve(arrayBuffer);
})
.catch((error) => {
reject(error);
});
})
.catch((error) => {
reject(error);
});
}
});
};
ChiptuneJsPlayer.prototype.play = async function (buffer: ArrayBuffer) {
this.unlock();
this.stop();
return this.createLibopenmptNode(buffer, this.buffer).then((processNode) => {
if (processNode === null) {
return;
}
this.libopenmpt._openmpt_module_set_repeat_count(
processNode.modulePtr,
this.config.repeatCount || 0,
);
this.currentPlayingNode = processNode;
processNode.connect(this.context);
this.context.connect(this.audioContext.destination);
});
};
ChiptuneJsPlayer.prototype.stop = function () {
if (this.currentPlayingNode != null) {
this.currentPlayingNode.disconnect();
this.currentPlayingNode.cleanup();
this.currentPlayingNode = null;
}
};
ChiptuneJsPlayer.prototype.togglePause = function () {
if (this.currentPlayingNode != null) {
this.currentPlayingNode.togglePause();
}
};
ChiptuneJsPlayer.prototype.getPattern = function () {
if (this.currentPlayingNode?.modulePtr) {
return this.libopenmpt._openmpt_module_get_current_pattern(
this.currentPlayingNode.modulePtr,
);
}
return 0;
};
ChiptuneJsPlayer.prototype.getRow = function () {
if (this.currentPlayingNode?.modulePtr) {
return this.libopenmpt._openmpt_module_get_current_row(
this.currentPlayingNode.modulePtr,
);
}
return 0;
};
ChiptuneJsPlayer.prototype.getNumPatterns = function () {
if (this.currentPlayingNode?.modulePtr) {
return this.libopenmpt._openmpt_module_get_num_patterns(
this.currentPlayingNode.modulePtr,
);
}
return 0;
};
ChiptuneJsPlayer.prototype.getPatternNumRows = function (pattern: number) {
if (this.currentPlayingNode?.modulePtr) {
return this.libopenmpt._openmpt_module_get_pattern_num_rows(
this.currentPlayingNode.modulePtr,
pattern,
);
}
return 0;
};
ChiptuneJsPlayer.prototype.getPatternRowChannel = function (
pattern: number,
row: number,
channel: number,
) {
if (this.currentPlayingNode?.modulePtr) {
return this.libopenmpt.UTF8ToString(
this.libopenmpt._openmpt_module_format_pattern_row_channel(
this.currentPlayingNode.modulePtr,
pattern,
row,
channel,
0,
true,
),
);
}
return "";
};
ChiptuneJsPlayer.prototype.createLibopenmptNode = async function (
buffer,
config: object,
) {
const maxFramesPerChunk = 4096;
const processNode = this.audioContext.createScriptProcessor(2048, 0, 2);
processNode.config = config;
processNode.player = this;
if (!this.libopenmpt) this.libopenmpt = await wasm();
const byteArray = new Int8Array(buffer);
const ptrToFile = this.libopenmpt._malloc(byteArray.byteLength);
this.libopenmpt.HEAPU8.set(byteArray, ptrToFile);
processNode.modulePtr = this.libopenmpt._openmpt_module_create_from_memory(
ptrToFile,
byteArray.byteLength,
0,
0,
0,
);
processNode.nbChannels = this.libopenmpt._openmpt_module_get_num_channels(
processNode.modulePtr,
);
processNode.patternIndex = -1;
processNode.paused = false;
processNode.leftBufferPtr = this.libopenmpt._malloc(4 * maxFramesPerChunk);
processNode.rightBufferPtr = this.libopenmpt._malloc(4 * maxFramesPerChunk);
processNode.cleanup = function () {
if (this.modulePtr !== 0) {
processNode.player.libopenmpt._openmpt_module_destroy(this.modulePtr);
this.modulePtr = 0;
}
if (this.leftBufferPtr !== 0) {
processNode.player.libopenmpt._free(this.leftBufferPtr);
this.leftBufferPtr = 0;
}
if (this.rightBufferPtr !== 0) {
processNode.player.libopenmpt._free(this.rightBufferPtr);
this.rightBufferPtr = 0;
}
};
processNode.stop = function () {
this.disconnect();
this.cleanup();
};
processNode.pause = function () {
this.paused = true;
};
processNode.unpause = function () {
this.paused = false;
};
processNode.togglePause = function () {
this.paused = !this.paused;
};
processNode.onaudioprocess = function (e) {
const outputL = e.outputBuffer.getChannelData(0);
const outputR = e.outputBuffer.getChannelData(1);
let framesToRender = outputL.length;
if (this.ModulePtr === 0) {
for (let i = 0; i < framesToRender; ++i) {
outputL[i] = 0;
outputR[i] = 0;
}
this.disconnect();
this.cleanup();
return;
}
if (this.paused) {
for (let i = 0; i < framesToRender; ++i) {
outputL[i] = 0;
outputR[i] = 0;
}
return;
}
let framesRendered = 0;
let ended = false;
let error = false;
const currentPattern =
processNode.player.libopenmpt._openmpt_module_get_current_pattern(
this.modulePtr,
);
const currentRow =
processNode.player.libopenmpt._openmpt_module_get_current_row(
this.modulePtr,
);
if (currentPattern !== this.patternIndex) {
processNode.player.fireEvent("onPatternChange");
}
processNode.player.fireEvent("onRowChange", { index: currentRow });
while (framesToRender > 0) {
const framesPerChunk = Math.min(framesToRender, maxFramesPerChunk);
const actualFramesPerChunk =
processNode.player.libopenmpt._openmpt_module_read_float_stereo(
this.modulePtr,
this.context.sampleRate,
framesPerChunk,
this.leftBufferPtr,
this.rightBufferPtr,
);
if (actualFramesPerChunk === 0) {
ended = true;
// modulePtr will be 0 on openmpt: error: openmpt_module_read_float_stereo: ERROR: module * not valid or other openmpt error
error = !this.modulePtr;
}
const rawAudioLeft = processNode.player.libopenmpt.HEAPF32.subarray(
this.leftBufferPtr / 4,
this.leftBufferPtr / 4 + actualFramesPerChunk,
);
const rawAudioRight = processNode.player.libopenmpt.HEAPF32.subarray(
this.rightBufferPtr / 4,
this.rightBufferPtr / 4 + actualFramesPerChunk,
);
for (let i = 0; i < actualFramesPerChunk; ++i) {
outputL[framesRendered + i] = rawAudioLeft[i];
outputR[framesRendered + i] = rawAudioRight[i];
}
for (let i = actualFramesPerChunk; i < framesPerChunk; ++i) {
outputL[framesRendered + i] = 0;
outputR[framesRendered + i] = 0;
}
framesToRender -= framesPerChunk;
framesRendered += framesPerChunk;
}
if (ended) {
this.disconnect();
this.cleanup();
error
? processNode.player.fireEvent("onError", { type: "openmpt" })
: processNode.player.fireEvent("onEnded");
}
};
return processNode;
};

View file

@ -25,14 +25,16 @@ export function openHelpMenu_(ev: MouseEvent) {
icon: "ph-lightbulb ph-bold ph-lg", icon: "ph-lightbulb ph-bold ph-lg",
to: "/about-firefish", to: "/about-firefish",
}, },
{ instance.tosUrl
? {
type: "button", type: "button",
text: i18n.ts.tos, text: i18n.ts.tos,
icon: "ph-scroll ph-bold ph-lg", icon: "ph-scroll ph-bold ph-lg",
action: () => { action: () => {
window.open(instance.tosUrl, "_blank"); window.open(instance.tosUrl, "_blank");
}, },
}, }
: null,
{ {
type: "button", type: "button",
text: i18n.ts.apps, text: i18n.ts.apps,

View file

@ -17,6 +17,21 @@ const menuOptions = [
"search", "search",
]; ];
export const defaultReactions = [
"⭐",
"❤️",
"😆",
"🤔",
"😮",
"🎉",
"💢",
"😥",
"😇",
"🥴",
"🔥",
"🐟",
];
// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう) // TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
// あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない // あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
export const defaultStore = markRaw( export const defaultStore = markRaw(
@ -83,19 +98,7 @@ export const defaultStore = markRaw(
}, },
reactions: { reactions: {
where: "account", where: "account",
default: [ default: defaultReactions,
"⭐",
"❤️",
"😆",
"🤔",
"😮",
"🎉",
"💢",
"😥",
"😇",
"🥴",
"🍮",
],
}, },
mutedWords: { mutedWords: {
where: "account", where: "account",

View file

@ -745,7 +745,7 @@ hr {
} }
@keyframes tada { @keyframes tada {
from { 0% {
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
} }
@ -767,14 +767,14 @@ hr {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
} }
to { 100% {
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
} }
} }
@media (prefers-reduced-motion) { @media (prefers-reduced-motion) {
@keyframes tada { @keyframes tada {
from { 0% {
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
} }
@ -782,7 +782,7 @@ hr {
transform: scale3d(1.1, 1.1, 1.1); transform: scale3d(1.1, 1.1, 1.1);
} }
to { 100% {
transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1);
} }
} }
@ -937,7 +937,7 @@ hr {
// } // }
@keyframes reset { @keyframes reset {
to { 100% {
transform: none; transform: none;
opacity: 1; opacity: 1;
} }
@ -948,13 +948,13 @@ hr {
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
@keyframes scaleIn { @keyframes scaleIn {
from { 0% {
transform: scale(0); transform: scale(0);
opacity: 0; opacity: 0;
} }
} }
@keyframes scaleInSmall { @keyframes scaleInSmall {
from { 0% {
transform: scale(0.8); transform: scale(0.8);
opacity: 0; opacity: 0;
} }

View file

@ -767,6 +767,9 @@ importers:
katex: katex:
specifier: 0.16.8 specifier: 0.16.8
version: 0.16.8 version: 0.16.8
libopenmpt-wasm:
specifier: github:TheEssem/libopenmpt-packaging#build
version: github.com/TheEssem/libopenmpt-packaging/d05d151a72b638c6312227af0417aca69521172c
matter-js: matter-js:
specifier: 0.19.0 specifier: 0.19.0
version: 0.19.0 version: 0.19.0
@ -891,7 +894,7 @@ importers:
devDependencies: devDependencies:
'@microsoft/api-documenter': '@microsoft/api-documenter':
specifier: ^7.22.21 specifier: ^7.22.21
version: 7.23.1(@types/node@20.3.1) version: 7.23.2(@types/node@20.3.1)
'@microsoft/api-extractor': '@microsoft/api-extractor':
specifier: ^7.36.0 specifier: ^7.36.0
version: 7.37.0(@types/node@20.3.1) version: 7.37.0(@types/node@20.3.1)
@ -1129,7 +1132,7 @@ packages:
dependencies: dependencies:
'@babel/compat-data': 7.22.20 '@babel/compat-data': 7.22.20
'@babel/helper-validator-option': 7.22.15 '@babel/helper-validator-option': 7.22.15
browserslist: 4.21.10 browserslist: 4.21.11
lru-cache: 5.1.1 lru-cache: 5.1.1
semver: 6.3.1 semver: 6.3.1
@ -2012,7 +2015,7 @@ packages:
'@typescript-eslint/parser': 6.7.2(eslint@8.49.0)(typescript@5.2.2) '@typescript-eslint/parser': 6.7.2(eslint@8.49.0)(typescript@5.2.2)
eslint: 8.49.0 eslint: 8.49.0
eslint-config-prettier: 9.0.0(eslint@8.49.0) eslint-config-prettier: 9.0.0(eslint@8.49.0)
eslint-plugin-jsdoc: 46.8.1(eslint@8.49.0) eslint-plugin-jsdoc: 46.8.2(eslint@8.49.0)
eslint-plugin-prettier: 5.0.0(eslint-config-prettier@9.0.0)(eslint@8.49.0)(prettier@3.0.3) eslint-plugin-prettier: 5.0.0(eslint-config-prettier@9.0.0)(eslint@8.49.0)(prettier@3.0.3)
eslint-plugin-tsdoc: 0.2.17 eslint-plugin-tsdoc: 0.2.17
eslint-plugin-vitest-globals: 1.4.0 eslint-plugin-vitest-globals: 1.4.0
@ -2207,8 +2210,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@microsoft/api-documenter@7.23.1(@types/node@20.3.1): /@microsoft/api-documenter@7.23.2(@types/node@20.3.1):
resolution: {integrity: sha512-QI8pafsD8jg6xlD18cHgvkRJQOocmFcAGjkvpc1QBKfUO7wgbqDO+nFKPLZSphuWfFKc+AzSHFooPswaAyHiLw==} resolution: {integrity: sha512-aylwXjDf98G8RhAZaosyFZaXsCAAKOA5S0u1R0FvNlomd6oPJ+lf/3ZqAfk/yR05L8cEtH8RmNfmMEa0sOCNOA==}
hasBin: true hasBin: true
dependencies: dependencies:
'@microsoft/api-extractor-model': 7.28.0(@types/node@20.3.1) '@microsoft/api-extractor-model': 7.28.0(@types/node@20.3.1)
@ -5151,18 +5154,18 @@ packages:
hasBin: true hasBin: true
dependencies: dependencies:
caniuse-db: 1.0.30001538 caniuse-db: 1.0.30001538
electron-to-chromium: 1.4.525 electron-to-chromium: 1.4.527
dev: true dev: true
/browserslist@4.21.10: /browserslist@4.21.11:
resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} resolution: {integrity: sha512-xn1UXOKUz7DjdGlg9RrUr0GGiWzI97UQJnugHtH0OLDfJB7jMgoIkYvRIEO1l9EeEERVqeqLYOcFBW9ldjypbQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
dependencies: dependencies:
caniuse-lite: 1.0.30001538 caniuse-lite: 1.0.30001538
electron-to-chromium: 1.4.525 electron-to-chromium: 1.4.527
node-releases: 2.0.13 node-releases: 2.0.13
update-browserslist-db: 1.0.12(browserslist@4.21.10) update-browserslist-db: 1.0.13(browserslist@4.21.11)
/buffer-alloc-unsafe@1.1.0: /buffer-alloc-unsafe@1.1.0:
resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==}
@ -5280,7 +5283,7 @@ packages:
dependencies: dependencies:
'@npmcli/fs': 3.1.0 '@npmcli/fs': 3.1.0
fs-minipass: 3.0.3 fs-minipass: 3.0.3
glob: 10.3.4 glob: 10.3.5
lru-cache: 7.18.3 lru-cache: 7.18.3
minipass: 7.0.3 minipass: 7.0.3
minipass-collect: 1.0.2 minipass-collect: 1.0.2
@ -6873,8 +6876,8 @@ packages:
dependencies: dependencies:
jake: 10.8.7 jake: 10.8.7
/electron-to-chromium@1.4.525: /electron-to-chromium@1.4.527:
resolution: {integrity: sha512-GIZ620hDK4YmIqAWkscG4W6RwY6gOx1y5J6f4JUQwctiJrqH2oxZYU4mXHi35oV32tr630UcepBzSBGJ/WYcZA==} resolution: {integrity: sha512-EafxEiEDzk2aLrdbtVczylHflHdHkNrpGNHIgDyA63sUQLQVS2ayj2hPw3RsVB42qkwURH+T2OxV7kGPUuYszA==}
/emittery@1.0.1: /emittery@1.0.1:
resolution: {integrity: sha512-2ID6FdrMD9KDLldGesP6317G78K7km/kMcwItRtVFva7I/cSEOIaLpewaUb+YLXVwdAp3Ctfxh/V5zIl1sj7dQ==} resolution: {integrity: sha512-2ID6FdrMD9KDLldGesP6317G78K7km/kMcwItRtVFva7I/cSEOIaLpewaUb+YLXVwdAp3Ctfxh/V5zIl1sj7dQ==}
@ -7376,8 +7379,8 @@ packages:
- supports-color - supports-color
dev: true dev: true
/eslint-plugin-jsdoc@46.8.1(eslint@8.49.0): /eslint-plugin-jsdoc@46.8.2(eslint@8.49.0):
resolution: {integrity: sha512-uTce7IBluPKXIQMWJkIwFsI1gv7sZRmLjctca2K5DIxPi8fSBj9f4iru42XmGwuiMyH2f3nfc60sFmnSGv4Z/A==} resolution: {integrity: sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
peerDependencies: peerDependencies:
eslint: ^7.0.0 || ^8.0.0 eslint: ^7.0.0 || ^8.0.0
@ -7447,7 +7450,7 @@ packages:
builtins: 5.0.1 builtins: 5.0.1
eslint: 8.49.0 eslint: 8.49.0
eslint-plugin-es-x: 7.2.0(eslint@8.49.0) eslint-plugin-es-x: 7.2.0(eslint@8.49.0)
get-tsconfig: 4.7.0 get-tsconfig: 4.7.1
ignore: 5.2.4 ignore: 5.2.4
is-core-module: 2.13.0 is-core-module: 2.13.0
minimatch: 3.1.2 minimatch: 3.1.2
@ -8595,8 +8598,8 @@ packages:
get-intrinsic: 1.2.1 get-intrinsic: 1.2.1
dev: true dev: true
/get-tsconfig@4.7.0: /get-tsconfig@4.7.1:
resolution: {integrity: sha512-pmjiZ7xtB8URYm74PlGJozDNyhvsVLUcpBa8DZBG3bWHwaHa9bPiRpiSfovw+fjhwONSCWKRyk+JQHEGZmMrzw==} resolution: {integrity: sha512-sLtd6Bcwbi9IrAow/raCOTE9pmhvo5ksQo5v2lApUGJMzja64MUYhBp0G6X1S+f7IrBPn1HP+XkS2w2meoGcjg==}
dependencies: dependencies:
resolve-pkg-maps: 1.0.0 resolve-pkg-maps: 1.0.0
dev: true dev: true
@ -8678,8 +8681,8 @@ packages:
- supports-color - supports-color
dev: true dev: true
/glob@10.3.4: /glob@10.3.5:
resolution: {integrity: sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==} resolution: {integrity: sha512-bYUpUD7XDEHI4Q2O5a7PXGvyw4deKR70kHiDxzQbe925wbZknhOzUt2xBgTkYL6RBcVeXYuD9iNYeqoWbBZQnA==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
hasBin: true hasBin: true
dependencies: dependencies:
@ -15345,13 +15348,13 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/update-browserslist-db@1.0.12(browserslist@4.21.10): /update-browserslist-db@1.0.13(browserslist@4.21.11):
resolution: {integrity: sha512-tE1smlR58jxbFMtrMpFNRmsrOXlpNXss965T1CrpwuZUzUAg/TBQc94SpyhDLSzrqrJS9xTRBthnZAGcE1oaxg==} resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
browserslist: '>= 4.21.0' browserslist: '>= 4.21.0'
dependencies: dependencies:
browserslist: 4.21.10 browserslist: 4.21.11
escalade: 3.1.1 escalade: 3.1.1
picocolors: 1.0.0 picocolors: 1.0.0
@ -15772,7 +15775,7 @@ packages:
'@webassemblyjs/wasm-parser': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6
acorn: 8.10.0 acorn: 8.10.0
acorn-import-assertions: 1.9.0(acorn@8.10.0) acorn-import-assertions: 1.9.0(acorn@8.10.0)
browserslist: 4.21.10 browserslist: 4.21.11
chrome-trace-event: 1.0.3 chrome-trace-event: 1.0.3
enhanced-resolve: 5.15.0 enhanced-resolve: 5.15.0
es-module-lexer: 1.3.1 es-module-lexer: 1.3.1
@ -16193,6 +16196,12 @@ packages:
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dev: false dev: false
github.com/TheEssem/libopenmpt-packaging/d05d151a72b638c6312227af0417aca69521172c:
resolution: {tarball: https://codeload.github.com/TheEssem/libopenmpt-packaging/tar.gz/d05d151a72b638c6312227af0417aca69521172c}
name: libopenmpt-wasm
version: 0.7.2
dev: true
github.com/misskey-dev/browser-image-resizer/5a70660c2ac8aad3d436bfa67a5e7f7c8946cac4: github.com/misskey-dev/browser-image-resizer/5a70660c2ac8aad3d436bfa67a5e7f7c8946cac4:
resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/5a70660c2ac8aad3d436bfa67a5e7f7c8946cac4} resolution: {tarball: https://codeload.github.com/misskey-dev/browser-image-resizer/tar.gz/5a70660c2ac8aad3d436bfa67a5e7f7c8946cac4}
name: '@misskey-dev/browser-image-resizer' name: '@misskey-dev/browser-image-resizer'