Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa5ee0a1d0 | |||
| d9930a9963 | |||
| 84539c5ac2 | |||
| 07ca7f6610 | |||
| 5d73f4bcb1 | |||
| 5f6d79fb1d | |||
| afb99e952e | |||
| 57c898c70c | |||
| 28f7e9ac97 | |||
| b6c178d243 | |||
| a5f1451d32 | |||
| 16bbeff1bf | |||
| 65856e18a4 | |||
| ca054327f7 | |||
| 2c0cebc7f8 | |||
| 503aa21cda | |||
| 97ec7b9b67 | |||
| c074d4468e | |||
| d5991ea0d4 | |||
| ab74459f36 | |||
| c892700e1c | |||
| ea96c54d23 | |||
| caf66ec3c6 | |||
| 994749c4e2 |
@@ -1,3 +0,0 @@
|
||||
# Copilot Instructions for Misskey
|
||||
|
||||
- en-US.yml を編集しないでください。
|
||||
@@ -54,110 +54,55 @@ jobs:
|
||||
BASE_MEMORY=$(cat ./artifacts/memory-base.json)
|
||||
HEAD_MEMORY=$(cat ./artifacts/memory-head.json)
|
||||
|
||||
variation() {
|
||||
calc() {
|
||||
BASE=$(echo "$BASE_MEMORY" | jq -r ".${1}.${2} // 0")
|
||||
HEAD=$(echo "$HEAD_MEMORY" | jq -r ".${1}.${2} // 0")
|
||||
BASE_RSS=$(echo "$BASE_MEMORY" | jq -r '.memory.rss // 0')
|
||||
HEAD_RSS=$(echo "$HEAD_MEMORY" | jq -r '.memory.rss // 0')
|
||||
|
||||
DIFF=$((HEAD - BASE))
|
||||
if [ "$BASE" -gt 0 ]; then
|
||||
DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE" | bc)
|
||||
else
|
||||
DIFF_PERCENT=0
|
||||
fi
|
||||
# Calculate difference
|
||||
if [ "$BASE_RSS" -gt 0 ] && [ "$HEAD_RSS" -gt 0 ]; then
|
||||
DIFF=$((HEAD_RSS - BASE_RSS))
|
||||
DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE_RSS" | bc)
|
||||
|
||||
# Convert KB to MB for readability
|
||||
BASE_MB=$(echo "scale=2; $BASE / 1024" | bc)
|
||||
HEAD_MB=$(echo "scale=2; $HEAD / 1024" | bc)
|
||||
DIFF_MB=$(echo "scale=2; $DIFF / 1024" | bc)
|
||||
# Convert to MB for readability
|
||||
BASE_MB=$(echo "scale=2; $BASE_RSS / 1048576" | bc)
|
||||
HEAD_MB=$(echo "scale=2; $HEAD_RSS / 1048576" | bc)
|
||||
DIFF_MB=$(echo "scale=2; $DIFF / 1048576" | bc)
|
||||
|
||||
JSON=$(jq -c -n \
|
||||
--argjson base "$BASE_MB" \
|
||||
--argjson head "$HEAD_MB" \
|
||||
--argjson diff "$DIFF_MB" \
|
||||
--argjson diff_percent "$DIFF_PERCENT" \
|
||||
'{base: $base, head: $head, diff: $diff, diff_percent: $diff_percent}')
|
||||
echo "base_mb=$BASE_MB" >> "$GITHUB_OUTPUT"
|
||||
echo "head_mb=$HEAD_MB" >> "$GITHUB_OUTPUT"
|
||||
echo "diff_mb=$DIFF_MB" >> "$GITHUB_OUTPUT"
|
||||
echo "diff_percent=$DIFF_PERCENT" >> "$GITHUB_OUTPUT"
|
||||
echo "has_data=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "$JSON"
|
||||
}
|
||||
|
||||
JSON=$(jq -c -n \
|
||||
--argjson VmRSS "$(calc $1 VmRSS)" \
|
||||
--argjson VmHWM "$(calc $1 VmHWM)" \
|
||||
--argjson VmSize "$(calc $1 VmSize)" \
|
||||
--argjson VmData "$(calc $1 VmData)" \
|
||||
'{VmRSS: $VmRSS, VmHWM: $VmHWM, VmSize: $VmSize, VmData: $VmData}')
|
||||
|
||||
echo "$JSON"
|
||||
}
|
||||
|
||||
JSON=$(jq -c -n \
|
||||
--argjson beforeGc "$(variation beforeGc)" \
|
||||
--argjson afterGc "$(variation afterGc)" \
|
||||
--argjson afterRequest "$(variation afterRequest)" \
|
||||
'{beforeGc: $beforeGc, afterGc: $afterGc, afterRequest: $afterRequest}')
|
||||
|
||||
echo "res=$JSON" >> "$GITHUB_OUTPUT"
|
||||
# Determine if this is a significant change (more than 5% increase)
|
||||
if [ "$(echo "$DIFF_PERCENT > 5" | bc)" -eq 1 ]; then
|
||||
echo "significant_increase=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "significant_increase=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "has_data=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- id: build-comment
|
||||
name: Build memory comment
|
||||
env:
|
||||
RES: ${{ steps.compare.outputs.res }}
|
||||
run: |
|
||||
HEADER="## Backend memory usage comparison"
|
||||
HEADER="## Backend Memory Usage Comparison"
|
||||
FOOTER="[See workflow logs for details](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
|
||||
|
||||
echo "$HEADER" > ./output.md
|
||||
echo >> ./output.md
|
||||
|
||||
table() {
|
||||
echo "| Metric | base (MB) | head (MB) | Diff (MB) | Diff (%) |" >> ./output.md
|
||||
echo "|--------|------:|------:|------:|------:|" >> ./output.md
|
||||
if [ "${{ steps.compare.outputs.has_data }}" == "true" ]; then
|
||||
echo "| Metric | base | head | Diff |" >> ./output.md
|
||||
echo "|--------|------|------|------|" >> ./output.md
|
||||
echo "| RSS | ${{ steps.compare.outputs.base_mb }} MB | ${{ steps.compare.outputs.head_mb }} MB | ${{ steps.compare.outputs.diff_mb }} MB (${{ steps.compare.outputs.diff_percent }}%) |" >> ./output.md
|
||||
echo >> ./output.md
|
||||
|
||||
line() {
|
||||
METRIC=$2
|
||||
BASE=$(echo "$RES" | jq -r ".${1}.${2}.base")
|
||||
HEAD=$(echo "$RES" | jq -r ".${1}.${2}.head")
|
||||
DIFF=$(echo "$RES" | jq -r ".${1}.${2}.diff")
|
||||
DIFF_PERCENT=$(echo "$RES" | jq -r ".${1}.${2}.diff_percent")
|
||||
|
||||
if (( $(echo "$DIFF_PERCENT > 0" | bc -l) )); then
|
||||
DIFF="+$DIFF"
|
||||
DIFF_PERCENT="+$DIFF_PERCENT"
|
||||
fi
|
||||
|
||||
# highlight VmRSS
|
||||
if [ "$2" = "VmRSS" ]; then
|
||||
METRIC="**${METRIC}**"
|
||||
BASE="**${BASE}**"
|
||||
HEAD="**${HEAD}**"
|
||||
DIFF="**${DIFF}**"
|
||||
DIFF_PERCENT="**${DIFF_PERCENT}**"
|
||||
fi
|
||||
|
||||
echo "| ${METRIC} | ${BASE} MB | ${HEAD} MB | ${DIFF} MB | ${DIFF_PERCENT}% |" >> ./output.md
|
||||
}
|
||||
|
||||
line $1 VmRSS
|
||||
line $1 VmHWM
|
||||
line $1 VmSize
|
||||
line $1 VmData
|
||||
}
|
||||
|
||||
echo "### Before GC" >> ./output.md
|
||||
table beforeGc
|
||||
echo >> ./output.md
|
||||
|
||||
echo "### After GC" >> ./output.md
|
||||
table afterGc
|
||||
echo >> ./output.md
|
||||
|
||||
echo "### After Request" >> ./output.md
|
||||
table afterRequest
|
||||
echo >> ./output.md
|
||||
|
||||
# Determine if this is a significant change (more than 5% increase)
|
||||
if [ "$(echo "$RES" | jq -r '.afterGc.VmRSS.diff_percent | tonumber > 5')" = "true" ]; then
|
||||
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
|
||||
if [ "${{ steps.compare.outputs.significant_increase }}" == "true" ]; then
|
||||
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
|
||||
echo >> ./output.md
|
||||
fi
|
||||
else
|
||||
echo "Could not retrieve memory usage data." >> ./output.md
|
||||
echo >> ./output.md
|
||||
fi
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
- Fix: ファイルタブのセンシティブメディアを開く際に確認ダイアログを出す設定が適用されない問題を修正
|
||||
- Fix: 2月29日を誕生日に設定している場合、閏年以外は3月1日を誕生日として扱うように修正
|
||||
- Fix: `Mk:C:container` の `borderWidth` が正しく反映されない問題を修正
|
||||
- Fix: mCaptchaが正しく動作しない問題を修正
|
||||
- Fix: 非ログイン時にリバーシの対局が表示されない問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: OAuthのクライアント情報取得(Client Information Discovery)において、IndieWeb Living Standard 11 July 2024で定義されているJSONドキュメント形式に対応しました
|
||||
|
||||
+4
-3
@@ -543,6 +543,7 @@ regenerate: "Regenera"
|
||||
fontSize: "Mida del text"
|
||||
mediaListWithOneImageAppearance: "Altura de la llista de fitxers amb una única imatge"
|
||||
limitTo: "Limita a {x}"
|
||||
showMediaListByGridInWideArea: "Mostra la llista de medis en vista quadrícula quan l'amplada de la pantalla ho permeti"
|
||||
noFollowRequests: "No tens sol·licituds de seguiment"
|
||||
openImageInNewTab: "Obre imatges a una nova pestanya"
|
||||
dashboard: "Tauler de control"
|
||||
@@ -1389,7 +1390,7 @@ defaultCompressionLevel_description: "Si el redueixes augmentaràs la qualitat d
|
||||
inMinutes: "Minut(s)"
|
||||
inDays: "Di(a)(es)"
|
||||
safeModeEnabled: "Mode segur activat"
|
||||
pluginsAreDisabledBecauseSafeMode: "Els afegits no estan activats perquè el mode segur està activat."
|
||||
pluginsAreDisabledBecauseSafeMode: "Les extensions no estan activades perquè el mode segur està activat."
|
||||
customCssIsDisabledBecauseSafeMode: "El CSS personalitzat no s'aplica perquè el mode segur es troba activat."
|
||||
themeIsDefaultBecauseSafeMode: "El tema predeterminat es farà servir mentre el mode segur estigui activat. Una vegada es desactivi el mode segur es restablirà el tema escollit."
|
||||
thankYouForTestingBeta: "Gràcies per ajudar-nos a provar la versió beta!"
|
||||
@@ -2184,8 +2185,8 @@ _email:
|
||||
title: "Has rebut una sol·licitud de seguiment"
|
||||
_plugin:
|
||||
install: "Instal·lar un afegit "
|
||||
installWarn: "Si us plau, no instal·lis afegits que no siguin de confiança."
|
||||
manage: "Gestionar els afegits"
|
||||
installWarn: "Si us plau, no instal·lis extensions que no siguin de confiança."
|
||||
manage: "Gestiona les extensions"
|
||||
viewSource: "Veure l'origen "
|
||||
viewLog: "Mostra el registre"
|
||||
_preferencesBackups:
|
||||
|
||||
@@ -1406,6 +1406,7 @@ youAreAdmin: "Sie sind ein Administrator"
|
||||
frame: "Rahmen"
|
||||
presets: "Vorlage"
|
||||
zeroPadding: "Nullauffüllung"
|
||||
nothingToConfigure: "Es sind keine Einstellungen verfügbar"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Dateibeschriftung"
|
||||
@@ -1421,12 +1422,22 @@ _imageEditing:
|
||||
camera_lens_model: "Objektivname"
|
||||
camera_mm: "Brennweite"
|
||||
camera_mm_35: "Brennweite (35-mm-Äquivalent)"
|
||||
gps_lat: "Breitengrad"
|
||||
gps_long: "Längengrad"
|
||||
_imageFrameEditor:
|
||||
header: "Kopfzeile"
|
||||
footer: "Fußzeile"
|
||||
centered: "Zentriert"
|
||||
backgroundColor: "Hintergrundfarbe"
|
||||
textColor: "Textfarbe"
|
||||
font: "Schriftart"
|
||||
fontSerif: "Serif"
|
||||
fontSansSerif: "Sans Serif"
|
||||
quitWithoutSaveConfirm: "Nicht gespeicherte Änderungen verwerfen?"
|
||||
_compression:
|
||||
_quality:
|
||||
medium: "Mittlere Qualität"
|
||||
low: "Niedrige Qualität"
|
||||
_order:
|
||||
newest: "Neueste zuerst"
|
||||
oldest: "Älteste zuerst"
|
||||
@@ -1519,6 +1530,7 @@ _settings:
|
||||
contentsUpdateFrequency_description2: "Wenn der Echtzeitmodus aktiviert ist, werden die Inhalte unabhängig von dieser Einstellung in Echtzeit aktualisiert."
|
||||
showUrlPreview: "URL-Vorschau anzeigen"
|
||||
showAvailableReactionsFirstInNote: "Zeige die verfügbaren Reaktionen im oberen Bereich an."
|
||||
enableAnimatedImages: "Animierte Bilder aktivieren"
|
||||
_chat:
|
||||
showSenderName: "Name des Absenders anzeigen"
|
||||
sendOnEnter: "Eingabetaste sendet Nachricht"
|
||||
@@ -1710,6 +1722,7 @@ _serverSettings:
|
||||
userGeneratedContentsVisibilityForVisitor: "Sichtbarkeit von nutzergenerierten Inhalten für Gäste"
|
||||
userGeneratedContentsVisibilityForVisitor_description: "Dies ist nützlich, um zu verhindern, dass unangemessene Inhalte, die nicht gut moderiert sind, ungewollt über deinen eigenen Server im Internet veröffentlicht werden."
|
||||
userGeneratedContentsVisibilityForVisitor_description2: "Die uneingeschränkte Veröffentlichung aller Inhalte des Servers im Internet, einschließlich der vom Server empfangenen Fremdinhalte, birgt Risiken. Dies ist besonders wichtig für Betrachter, die sich des dezentralen Charakters der Inhalte nicht bewusst sind, da sie selbst fremde Inhalte fälschlicherweise als auf dem Server erstellte Inhalte wahrnehmen könnten."
|
||||
restartServerSetupWizardConfirm_text: "Einige aktuelle Einstellungen werden zurückgesetzt."
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "Alles ist öffentlich"
|
||||
localOnly: "Nur lokale Inhalte werden veröffentlicht, fremde Inhalte bleiben privat"
|
||||
@@ -2318,6 +2331,7 @@ _time:
|
||||
minute: "Minute(n)"
|
||||
hour: "Stunde(n)"
|
||||
day: "Tag(en)"
|
||||
month: "Monat(e)"
|
||||
_2fa:
|
||||
alreadyRegistered: "Du hast bereits ein Gerät für Zwei-Faktor-Authentifizierung registriert."
|
||||
registerTOTP: "Authentifizierungs-App registrieren"
|
||||
@@ -2447,6 +2461,7 @@ _auth:
|
||||
scopeUser: "Als folgender Benutzer agieren"
|
||||
pleaseLogin: "Bitte logge dich ein, um Apps zu authorisieren."
|
||||
byClickingYouWillBeRedirectedToThisUrl: "Wenn der Zugang gewährt wird, wirst du automatisch zu folgender URL weitergeleitet"
|
||||
alreadyAuthorized: "Dieser Anwendung wurde bereits Zugriff gewährt."
|
||||
_antennaSources:
|
||||
all: "Alle Notizen"
|
||||
homeTimeline: "Notizen von Benutzern, denen gefolgt wird"
|
||||
@@ -2495,11 +2510,28 @@ _widgets:
|
||||
chat: "Mit dem Benutzer chatten"
|
||||
_widgetOptions:
|
||||
showHeader: "Kopfzeile anzeigen"
|
||||
transparent: "Hintergrund transparent machen"
|
||||
height: "Höhe"
|
||||
_button:
|
||||
colored: "Farbig"
|
||||
_clock:
|
||||
size: "Größe"
|
||||
thicknessThin: "Dünn"
|
||||
thicknessMedium: "Normal"
|
||||
thicknessThick: "Dick"
|
||||
twentyFour: "24-Stunden-Format"
|
||||
labelTime: "Uhrzeit"
|
||||
labelTz: "Zeitzone"
|
||||
timezone: "Zeitzone"
|
||||
showMs: "Millisekunden anzeigen"
|
||||
_jobQueue:
|
||||
sound: "Ton abspielen"
|
||||
_rss:
|
||||
url: "RSS-Feed-URL"
|
||||
refreshIntervalSec: "Aktualisierungsintervall (Sekunden)"
|
||||
maxEntries: "Maximale Anzahl der angezeigten Einträge"
|
||||
_rssTicker:
|
||||
shuffle: "Zufällige Anzeigereihenfolge"
|
||||
_birthdayFollowings:
|
||||
period: "Dauer"
|
||||
_cw:
|
||||
@@ -2694,6 +2726,8 @@ _notification:
|
||||
youReceivedFollowRequest: "Du hast eine Follow-Anfrage erhalten"
|
||||
yourFollowRequestAccepted: "Deine Follow-Anfrage wurde akzeptiert"
|
||||
pollEnded: "Umfrageergebnisse sind verfügbar"
|
||||
scheduledNotePosted: "Geplante Notiz wurde veröffentlicht"
|
||||
scheduledNotePostFailed: "Veröffentlichen der geplanten Notiz fehlgeschlagen"
|
||||
newNote: "Neue Notiz"
|
||||
unreadAntennaNote: "Antenne {name}"
|
||||
roleAssigned: "Rolle zugewiesen"
|
||||
@@ -2762,6 +2796,9 @@ _deck:
|
||||
usedAsMinWidthWhenFlexible: "Ist \"Automatische Breitenanpassung\" aktiviert, wird hierfür die minimale Breite verwendet"
|
||||
flexible: "Automatische Breitenanpassung"
|
||||
enableSyncBetweenDevicesForProfiles: "Aktivieren der Synchronisierung von Profilinformationen zwischen Geräten"
|
||||
_howToUse:
|
||||
addColumn_title: "Spalte hinzufügen"
|
||||
settings_title: "UI-Einstellungen"
|
||||
_columns:
|
||||
main: "Hauptspalte"
|
||||
widgets: "Widgets"
|
||||
@@ -2822,6 +2859,8 @@ _abuseReport:
|
||||
notifiedWebhook: "Zu verwendender Webhook"
|
||||
deleteConfirm: "Bist du sicher, dass du den Empfänger der Benachrichtigung entfernen möchtest?"
|
||||
_moderationLogTypes:
|
||||
clearQueue: "Warteschlange leeren"
|
||||
promoteQueue: "Warteschlange erneut ausführen"
|
||||
createRole: "Rolle erstellt"
|
||||
deleteRole: "Rolle gelöscht"
|
||||
updateRole: "Rolle aktualisiert"
|
||||
@@ -3131,6 +3170,7 @@ _bootErrors:
|
||||
otherOption1: "Client-Einstellungen und Cache löschen"
|
||||
otherOption2: "Einfachen Client starten"
|
||||
otherOption3: "Starte das Reparaturwerkzeug"
|
||||
otherOption4: "Misskey im abgesicherten Modus starten"
|
||||
_search:
|
||||
searchScopeAll: "Alle"
|
||||
searchScopeLocal: "Lokal"
|
||||
@@ -3227,16 +3267,20 @@ _watermarkEditor:
|
||||
polkadotSubDotOpacity: "Deckkraft des Unterpunktes"
|
||||
polkadotSubDotRadius: "Größe des Unterpunktes"
|
||||
polkadotSubDotDivisions: "Anzahl der Unterpunkte"
|
||||
failedToLoadImage: "Bild konnte nicht geladen werden"
|
||||
_imageEffector:
|
||||
title: "Effekte"
|
||||
addEffect: "Effekte hinzufügen"
|
||||
discardChangesConfirm: "Änderungen verwerfen und beenden?"
|
||||
failedToLoadImage: "Bild konnte nicht geladen werden"
|
||||
_fxs:
|
||||
chromaticAberration: "Chromatische Abweichung"
|
||||
glitch: "Glitch"
|
||||
mirror: "Spiegeln"
|
||||
invert: "Farben umkehren"
|
||||
grayscale: "Schwarzweiß"
|
||||
blur: "Verwischen"
|
||||
pixelate: "Verpixeln"
|
||||
colorAdjust: "Farbkorrektur"
|
||||
colorClamp: "Farbkomprimierung"
|
||||
colorClampAdvanced: "Farbkomprimierung (erweitert)"
|
||||
@@ -3251,6 +3295,11 @@ _imageEffector:
|
||||
color: "Farbe"
|
||||
opacity: "Transparenz"
|
||||
lightness: "Erhellen"
|
||||
saturation: "Sättigung"
|
||||
max: "Maximum"
|
||||
min: "Minimum"
|
||||
direction: "Richtung"
|
||||
frequency: "Häufigkeit"
|
||||
drafts: "Entwurf"
|
||||
_drafts:
|
||||
select: "Entwurf auswählen"
|
||||
|
||||
@@ -543,6 +543,7 @@ regenerate: "Regenerate"
|
||||
fontSize: "Font size"
|
||||
mediaListWithOneImageAppearance: "Height of media lists with one image only"
|
||||
limitTo: "Limit to {x}"
|
||||
showMediaListByGridInWideArea: "Display the media list in a grid when the screen width is wide"
|
||||
noFollowRequests: "You don't have any pending follow requests"
|
||||
openImageInNewTab: "Open images in new tab"
|
||||
dashboard: "Dashboard"
|
||||
|
||||
@@ -543,6 +543,7 @@ regenerate: "Regenerar"
|
||||
fontSize: "Tamaño de la letra"
|
||||
mediaListWithOneImageAppearance: "Altura de la lista de medios con una sola imagen."
|
||||
limitTo: "{x} hasta un máximo de"
|
||||
showMediaListByGridInWideArea: "Cuando el ancho de la pantalla sea grande, muestra la lista de multimedia uno al lado del otro."
|
||||
noFollowRequests: "No hay solicitudes de seguimiento"
|
||||
openImageInNewTab: "Abrir imagen en nueva pestaña"
|
||||
dashboard: "Panel de control"
|
||||
|
||||
+10
-9
@@ -543,6 +543,7 @@ regenerate: "Generare di nuovo"
|
||||
fontSize: "Dimensione carattere"
|
||||
mediaListWithOneImageAppearance: "Altezza dell'elenco media con una sola immagine "
|
||||
limitTo: "Limita a {x}"
|
||||
showMediaListByGridInWideArea: "Quando la larghezza dello schermo è ampia, mostra i media affiancati"
|
||||
noFollowRequests: "Non ci sono richieste di relazione"
|
||||
openImageInNewTab: "Apri le immagini in un nuovo tab"
|
||||
dashboard: "Pannello di controllo"
|
||||
@@ -556,7 +557,7 @@ clientSettings: "Impostazioni client"
|
||||
accountSettings: "Impostazioni profilo"
|
||||
promotion: "Promossa"
|
||||
promote: "Pubblicizza"
|
||||
numberOfDays: "Numero di giorni"
|
||||
numberOfDays: ""
|
||||
hideThisNote: "Nasconda la nota"
|
||||
showFeaturedNotesInTimeline: "Mostrare le note di tendenza nella tua timeline"
|
||||
objectStorage: "Storage S3"
|
||||
@@ -701,7 +702,7 @@ hardWordMuteDescription: "Ignora le Note con la parola o la regola specificata.
|
||||
regexpError: "errore regex"
|
||||
regexpErrorDescription: "Si è verificato un errore nell'espressione regolare alla riga {line} della parola muta {tab}:"
|
||||
instanceMute: "Silenziare l'istanza"
|
||||
userSaysSomething: "{name} ha detto qualcosa"
|
||||
userSaysSomething: "{name} ha scritto qualcosa"
|
||||
userSaysSomethingAbout: "{name} ha anNotato qualcosa su \"{word}\""
|
||||
makeActive: "Attiva"
|
||||
display: "Visualizza"
|
||||
@@ -2070,7 +2071,7 @@ _role:
|
||||
gtlAvailable: "Disponibilità della Timeline Federata"
|
||||
ltlAvailable: "Disponibilità della Timeline Locale"
|
||||
canPublicNote: "Scrivere Note con Visibilità Pubblica"
|
||||
mentionMax: "Numero massimo di menzioni in una nota"
|
||||
mentionMax: ""
|
||||
canInvite: "Generare codici di invito all'istanza"
|
||||
inviteLimit: "Limite di codici invito"
|
||||
inviteLimitCycle: "Intervallo di emissione del codice di invito"
|
||||
@@ -2436,7 +2437,7 @@ _permissions:
|
||||
"read:flash-likes": "Visualizza lista di Play piaciuti"
|
||||
"write:flash-likes": "Modifica lista di Play piaciuti"
|
||||
"read:admin:abuse-user-reports": "Mostra i report dai profili utente"
|
||||
"write:admin:delete-account": "Elimina l'account utente"
|
||||
"write:admin:delete-account": "Elimina l'utenza"
|
||||
"write:admin:delete-all-files-of-a-user": "Elimina i file dell'account utente"
|
||||
"read:admin:index-stats": "Visualizza informazioni sugli indici del database"
|
||||
"read:admin:table-stats": "Visualizza informazioni sulle tabelle del database"
|
||||
@@ -2532,19 +2533,19 @@ _widgets:
|
||||
instanceCloud: "Nuvola di federazione"
|
||||
postForm: "Finestra di pubblicazione"
|
||||
slideshow: "Diapositive"
|
||||
button: "Pulsante"
|
||||
button: "Bottone"
|
||||
onlineUsers: "Persone attive adesso"
|
||||
jobQueue: "Coda di lavoro"
|
||||
serverMetric: "Statistiche server"
|
||||
aiscript: "Console AiScript"
|
||||
aiscriptApp: "App AiScript"
|
||||
aichan: "Mascotte Ai"
|
||||
userList: "Elenco utenti"
|
||||
userList: "Lista profili"
|
||||
_userList:
|
||||
chooseList: "Seleziona una lista"
|
||||
clicker: "Cliccheria"
|
||||
birthdayFollowings: "Compleanni del giorno"
|
||||
chat: "Chatta con questa persona"
|
||||
chat: "Messaggi diretti"
|
||||
_widgetOptions:
|
||||
showHeader: "Mostra la testata"
|
||||
transparent: "Sfondo trasparente"
|
||||
@@ -2660,7 +2661,7 @@ _profile:
|
||||
metadataContent: "Contenuto"
|
||||
changeAvatar: "Modifica immagine profilo"
|
||||
changeBanner: "Cambia intestazione"
|
||||
verifiedLinkDescription: "Puoi verificare il tuo profilo mostrando una icona. Devi inserire la URL alla pagina che contiene un link al tuo profilo.\nPer verificare il profilo tramite la spunta di conferma, devi inserire la url alla pagina che contiene un link al tuo profilo Misskey. Deve avere attributo rel='me'."
|
||||
verifiedLinkDescription: "Come avere i collegamenti verificati: inserisci la URL ad una pagina che contiene un collegamento al tuo profilo.\nVedrai una spunta di conferma se, in quella pagina, il collegamento al tuo profilo Misskey ha attributo rel='me'."
|
||||
avatarDecorationMax: "Puoi aggiungere fino a {max} decorazioni."
|
||||
followedMessage: "Messaggio, quando qualcuno ti segue"
|
||||
followedMessageDescription: "Puoi impostare un breve messaggio da mostrare agli altri profili quando ti seguono."
|
||||
@@ -2780,7 +2781,7 @@ _notification:
|
||||
fileUploaded: "File caricato correttamente"
|
||||
youGotMention: "{name} ti ha menzionato"
|
||||
youGotReply: "{name} ti ha risposto"
|
||||
youGotQuote: "{name} ha citato la tua Nota e ha detto"
|
||||
youGotQuote: "{name} ha scritto citando la tua Nota"
|
||||
youRenoted: "{name} ha rinotato"
|
||||
youWereFollowed: "Follower aggiuntivo"
|
||||
youReceivedFollowRequest: "Hai ricevuto una richiesta di follow"
|
||||
|
||||
@@ -1778,12 +1778,6 @@ _serverSettings:
|
||||
entrancePageStyle: "エントランスページのスタイル"
|
||||
showTimelineForVisitor: "タイムラインを表示する"
|
||||
showActivitiesForVisitor: "アクティビティを表示する"
|
||||
features: "機能"
|
||||
|
||||
_spReactions:
|
||||
enable: "スペシャルリアクションを有効にする"
|
||||
description1: "通常のリアクションより目立つ「スペシャルリアクション」をノートに送れる機能です。"
|
||||
description2: "有効にする場合、ロールポリシーで、毎月送ることのできる最大数を設定してください。"
|
||||
|
||||
_userGeneratedContentsVisibilityForVisitor:
|
||||
all: "全て公開"
|
||||
@@ -2896,7 +2890,6 @@ _notification:
|
||||
renote: "リノート"
|
||||
quote: "引用"
|
||||
reaction: "リアクション"
|
||||
spReaction: "スペシャルリアクション"
|
||||
pollEnded: "アンケートが終了"
|
||||
scheduledNotePosted: "予約投稿が成功した"
|
||||
scheduledNotePostFailed: "予約投稿が失敗した"
|
||||
|
||||
+9
-8
@@ -543,6 +543,7 @@ regenerate: "재생성"
|
||||
fontSize: "글자 크기"
|
||||
mediaListWithOneImageAppearance: "이미지가 1개 뿐인 미디어 목록의 높이"
|
||||
limitTo: "{x}로 제한"
|
||||
showMediaListByGridInWideArea: "화면 폭이 넓을 때는 미디어 목록을 가로로 표시하기"
|
||||
noFollowRequests: "처리되지 않은 팔로우 요청이 없습니다"
|
||||
openImageInNewTab: "새 탭에서 이미지 열기"
|
||||
dashboard: "대시보드"
|
||||
@@ -1334,7 +1335,7 @@ markAsSensitiveConfirm: "이 미디어를 민감한 미디어로 설정하시겠
|
||||
unmarkAsSensitiveConfirm: "이 미디어의 민감한 미디어 지정을 해제하시겠습니까?"
|
||||
preferences: "환경설정"
|
||||
accessibility: "접근성"
|
||||
preferencesProfile: "설정 프로필"
|
||||
preferencesProfile: "설정 프로파일"
|
||||
copyPreferenceId: "설정한 ID를 복사"
|
||||
resetToDefaultValue: "기본값으로 되돌리기"
|
||||
overrideByAccount: "계정으로 덮어쓰기"
|
||||
@@ -1347,7 +1348,7 @@ preferenceSyncConflictTitle: "서버에 설정값이 존재합니다."
|
||||
preferenceSyncConflictText: "동기화를 활성화 한 항목의 설정 값은 서버에 저장되지만, 해당 항목은 이미 서버에 설정 값이 저장되어져 있습니다. 어느 쪽의 설정 값을 덮어씌울까요?"
|
||||
preferenceSyncConflictChoiceMerge: "병합"
|
||||
preferenceSyncConflictChoiceServer: "서버 설정값"
|
||||
preferenceSyncConflictChoiceDevice: "장치 설정값"
|
||||
preferenceSyncConflictChoiceDevice: "장치 설정 값"
|
||||
preferenceSyncConflictChoiceCancel: "동기화 취소"
|
||||
paste: "붙여넣기"
|
||||
emojiPalette: "이모지 팔레트"
|
||||
@@ -1558,11 +1559,11 @@ _settings:
|
||||
showSenderName: "발신자 이름 표시"
|
||||
sendOnEnter: "엔터로 보내기"
|
||||
_preferencesProfile:
|
||||
profileName: "프로필 이름"
|
||||
profileName: "프로파일 이름"
|
||||
profileNameDescription: "이 디바이스를 식별할 이름을 설정해 주세요."
|
||||
profileNameDescription2: "예: '메인PC', '스마트폰' 등"
|
||||
manageProfiles: "프로파일 관리"
|
||||
shareSameProfileBetweenDevicesIsNotRecommended: "여러 장치에서 동일한 프로필을 공유하는 것은 권장하지 않습니다."
|
||||
shareSameProfileBetweenDevicesIsNotRecommended: "여러 장치에서 같은 프로파일을 공유하는 것은 권장하지 않습니다."
|
||||
useSyncBetweenDevicesOptionIfYouWantToSyncSetting: "여러 장치에서 동기화하고 싶은 설정 항목이 있는 경우에는 개별로 '여러 장치에서 동기화' 옵션을 활성화해 주십시오."
|
||||
_preferencesBackup:
|
||||
autoBackup: "자동 백업"
|
||||
@@ -1570,7 +1571,7 @@ _preferencesBackup:
|
||||
noBackupsFoundTitle: "백업을 찾을 수 없습니다"
|
||||
noBackupsFoundDescription: "자동으로 생성된 백업은 찾을 수 없었지만, 수동으로 백업 파일을 저장한 경우 해당 파일을 가져와 복원할 수 있습니다."
|
||||
selectBackupToRestore: "복원할 백업을 선택하세요"
|
||||
youNeedToNameYourProfileToEnableAutoBackup: "자동 백업을 활성화하려면 프로필 이름을 설정해야 합니다."
|
||||
youNeedToNameYourProfileToEnableAutoBackup: "자동 백업을 활성화하려면 프로파일 이름을 설정해야 합니다."
|
||||
autoPreferencesBackupIsNotEnabledForThisDevice: "이 장치에서 설정 자동 백업이 활성화되어 있지 않습니다."
|
||||
backupFound: "설정 백업이 발견되었습니다"
|
||||
forceBackup: "설정 강제 백업"
|
||||
@@ -2857,15 +2858,15 @@ _deck:
|
||||
useSimpleUiForNonRootPages: "루트 이외의 페이지로 접속한 경우 UI 간략화하기"
|
||||
usedAsMinWidthWhenFlexible: "'폭 자동 조정'이 활성화된 경우 최소 폭으로 사용됩니다"
|
||||
flexible: "폭 자동 조정"
|
||||
enableSyncBetweenDevicesForProfiles: "프로파일 정보의 디바이스 간 동기화를 활성화"
|
||||
enableSyncBetweenDevicesForProfiles: "프로파일 정보의 장치 간 동기화를 활성화"
|
||||
showHowToUse: "UI 설명 보기"
|
||||
_howToUse:
|
||||
addColumn_title: "칼럼 추가"
|
||||
addColumn_description: "칼럼의 종류를 선택해 추가할 수 있습니다."
|
||||
settings_title: "UI 설정"
|
||||
settings_description: "덱 UI의 상세 설정을 할 수 있습니다."
|
||||
switchProfile_title: "프로필 전환"
|
||||
switchProfile_description: "UI의 레이아웃을 프로필로 저장하고, 언제나 전환할 수 있습니다."
|
||||
switchProfile_title: "프로파일 전환"
|
||||
switchProfile_description: "UI의 레이아웃을 프로파일로 저장하고 언제든지 전환할 수 있습니다."
|
||||
_columns:
|
||||
main: "메인"
|
||||
widgets: "위젯"
|
||||
|
||||
@@ -1231,6 +1231,7 @@ showRepliesToOthersInTimelineAll: "Показывать в ленте ответ
|
||||
hideRepliesToOthersInTimelineAll: "Скрывать в ленте ответы пользователей, на которых вы подписаны"
|
||||
confirmShowRepliesAll: "Это нельзя будет отменить. Показать ответы от всех, на кого вы подписаны?"
|
||||
confirmHideRepliesAll: "Это нельзя будет отменить. Скрыть ответы от всех, на кого вы подписаны?"
|
||||
externalServices: "Интеграции"
|
||||
sourceCode: "Исходный код"
|
||||
sourceCodeIsNotYetProvided: "Исходный код пока не доступен. Свяжитесь с администратором, чтобы исправить эту проблему."
|
||||
repositoryUrl: "Ссылка на репозиторий"
|
||||
|
||||
+131
-19
@@ -83,6 +83,8 @@ files: "ไฟล์"
|
||||
download: "ดาวน์โหลด"
|
||||
driveFileDeleteConfirm: "ต้องการลบไฟล์ “{name}” ใช่ไหม? โน้ตที่แนบมากับไฟล์นี้ก็จะถูกลบไปด้วย"
|
||||
unfollowConfirm: "ต้องการเลิกติดตาม {name} ใช่ไหม?"
|
||||
cancelFollowRequestConfirm: "ยกเลิกคำขอติดตาม {name} ใช่ไหม?"
|
||||
rejectFollowRequestConfirm: "ปฏิเสธคำขอติดตามจาก {name} ใช่ไหม?"
|
||||
exportRequested: "คุณได้ร้องขอการส่งออก อาจใช้เวลาสักครู่ และจะถูกเพิ่มในไดรฟ์ของคุณเมื่อเสร็จสิ้นแล้ว"
|
||||
importRequested: "คุณได้ร้องขอการนำเข้า การดำเนินการนี้อาจใช้เวลาสักครู่"
|
||||
lists: "รายชื่อ"
|
||||
@@ -204,7 +206,7 @@ host: "โฮสต์"
|
||||
selectSelf: "เลือกตัวเอง"
|
||||
selectUser: "เลือกผู้ใช้งาน"
|
||||
recipient: "ผู้รับ"
|
||||
annotation: "หมายเหตุประกอบ"
|
||||
annotation: "ข้อความเกริ่น"
|
||||
federation: "สหพันธ์"
|
||||
instances: "เซิร์ฟเวอร์"
|
||||
registeredAt: "วันที่ลงทะเบียน"
|
||||
@@ -222,7 +224,7 @@ operations: "ดำเนินการ"
|
||||
software: "ซอฟต์แวร์"
|
||||
softwareName: "ชื่อซอฟต์แวร์"
|
||||
version: "เวอร์ชั่น"
|
||||
metadata: "Metadata"
|
||||
metadata: "เมทาเดต้า"
|
||||
withNFiles: "{n} ไฟล์"
|
||||
monitor: "มอนิเตอร์"
|
||||
jobQueue: "คิวงาน"
|
||||
@@ -302,6 +304,7 @@ uploadFromUrlMayTakeTime: "การอัปโหลดอาจใช้เ
|
||||
uploadNFiles: "อัปโหลด {n} ไฟล์"
|
||||
explore: "สำรวจ"
|
||||
messageRead: "อ่านแล้ว"
|
||||
readAllChatMessages: "ทำเครื่องหมายใส่ข้อความทั้งหมดว่าอ่านแล้ว"
|
||||
noMoreHistory: "ไม่มีประวัติเพิ่มเติม"
|
||||
startChat: "เริ่มแชต"
|
||||
nUsersRead: "อ่านโดย {n}"
|
||||
@@ -334,6 +337,7 @@ fileName: "ชื่อไฟล์"
|
||||
selectFile: "เลือกไฟล์"
|
||||
selectFiles: "เลือกไฟล์"
|
||||
selectFolder: "เลือกโฟลเดอร์"
|
||||
unselectFolder: "ยกเลิกการเลือกโฟลเดอร์"
|
||||
selectFolders: "เลือกโฟลเดอร์"
|
||||
fileNotSelected: "ยังไม่ได้เลือกไฟล์"
|
||||
renameFile: "เปลี่ยนชื่อไฟล์"
|
||||
@@ -346,6 +350,7 @@ addFile: "เพิ่มไฟล์"
|
||||
showFile: "แสดงไฟล์"
|
||||
emptyDrive: "ไดรฟ์ของคุณว่างเปล่านะ"
|
||||
emptyFolder: "โฟลเดอร์นี้ว่างเปล่า"
|
||||
dropHereToUpload: "ดรอปไฟล์ลงที่นี่เพื่ออัปโหลด"
|
||||
unableToDelete: "ไม่สามารถลบออกได้"
|
||||
inputNewFileName: "ป้อนชื่อไฟล์ใหม่"
|
||||
inputNewDescription: "กรุณาใส่แคปชั่นใหม่"
|
||||
@@ -428,7 +433,7 @@ antennaKeywordsDescription: "คั่นด้วยเว้นวรรคส
|
||||
notifyAntenna: "แจ้งเตือนเกี่ยวกับโน้ตใหม่"
|
||||
withFileAntenna: "เฉพาะโน้ตที่มีไฟล์"
|
||||
excludeNotesInSensitiveChannel: "ไม่รวมโน้ตจากช่องเนื้อหาละเอียดอ่อน"
|
||||
enableServiceworker: "เปิดใช้งานการแจ้งเตือนแบบพุชไปยังเบราว์เซอร์ของคุณ"
|
||||
enableServiceworker: "เปิดใช้งานการแจ้งเตือนแบบพุชไปยังเบราว์เซอร์"
|
||||
antennaUsersDescription: "ระบุหนึ่งชื่อผู้ใช้ต่อบรรทัด"
|
||||
caseSensitive: "อักษรพิมพ์ใหญ่-พิมพ์เล็กความหมายต่างกัน"
|
||||
withReplies: "รวมตอบกลับ"
|
||||
@@ -538,6 +543,7 @@ regenerate: "สร้างอีกครั้ง"
|
||||
fontSize: "ขนาดตัวอักษร"
|
||||
mediaListWithOneImageAppearance: "ความสูงของรายการสื่อที่มีเพียงรูปเดียว"
|
||||
limitTo: "จำกัดไว้ที่ {x}"
|
||||
showMediaListByGridInWideArea: "เมื่อหน้าจอกว้างยาวขึ้น ให้เรียงรายการสื่อเป็นแนวนอน"
|
||||
noFollowRequests: "คุณไม่มีคำขอติดตามที่รอดำเนินการ"
|
||||
openImageInNewTab: "เปิดรูปภาพในแท็บใหม่"
|
||||
dashboard: "หน้ากระดานหลัก"
|
||||
@@ -612,7 +618,7 @@ uiInspectorDescription: "คุณสามารถตรวจสอบรา
|
||||
output: "เอาท์พุต"
|
||||
script: "สคริปต์"
|
||||
disablePagesScript: "ปิดการใช้งาน AiScript บนเพจ"
|
||||
updateRemoteUser: "อัปเดตข้อมูลผู้ใช้งานระยะไกล"
|
||||
updateRemoteUser: "อัปเดตข้อมูลผู้ใช้ระยะไกล"
|
||||
unsetUserAvatar: "เลิกตั้งไอคอน"
|
||||
unsetUserAvatarConfirm: "ต้องการเลิกตั้งไอคอนประจำตัวหรือไม่?"
|
||||
unsetUserBanner: "เลิกตั้งแบนเนอร์"
|
||||
@@ -773,6 +779,7 @@ lockedAccountInfo: "แม้ว่าการอนุมัติการต
|
||||
alwaysMarkSensitive: "ทำเครื่องหมายว่ามีเนื้อหาละเอียดอ่อนเป็นค่าเริ่มต้น"
|
||||
loadRawImages: "โหลดภาพต้นฉบับแทนการแสดงภาพขนาดย่อ"
|
||||
disableShowingAnimatedImages: "ไม่ต้องเล่นภาพเคลื่อนไหว"
|
||||
disableShowingAnimatedImages_caption: "หากภาพเคลื่อนไหวไม่เล่นแม่จะปิดตั้งค่านี้ไปแล้ว อาจเป็นกรณีที่การตั้งค่าการช่วยการเข้าถึงหรือการประหยัดพลังงาน ของเบราว์เซอร์/OS เข้าแทรกแซง"
|
||||
highlightSensitiveMedia: "ไฮไลท์สื่อที่มีเนื้อหาละเอียดอ่อน"
|
||||
verificationEmailSent: "ได้ส่งอีเมลยืนยันแล้ว กรุณาเข้าลิงก์ที่ระบุในอีเมลเพื่อทำการตั้งค่าให้เสร็จสิ้น"
|
||||
notSet: "ไม่ได้ตั้งค่า"
|
||||
@@ -887,7 +894,7 @@ low: "ต่ำ"
|
||||
emailNotConfiguredWarning: "ยังไม่ได้ตั้งค่าที่อยู่อีเมล"
|
||||
ratio: "อัตราส่วน"
|
||||
previewNoteText: "แสดงตัวอย่าง"
|
||||
customCss: "CSS ที่กำหนดเอง"
|
||||
customCss: "CSS แบบกำหนดเอง"
|
||||
customCssWarn: "ควรใช้การตั้งค่านี้เฉพาะต่อเมื่อคุณรู้มันใช้ทำอะไร การตั้งค่าที่ไม่เหมาะสมอาจทำให้ไคลเอ็นต์ไม่สามารถใช้งานได้อย่างถูกต้อง"
|
||||
global: "ทั่วโลก"
|
||||
squareAvatars: "แสดงไอคอนประจำตัวเป็นสี่เหลี่ยม"
|
||||
@@ -930,7 +937,7 @@ unmuteThread: "เลิกปิดเสียงเธรด"
|
||||
followingVisibility: "การมองเห็นที่เรากำลังติดตาม"
|
||||
followersVisibility: "การมองเห็นผู้ที่กำลังติดตามเรา"
|
||||
continueThread: "ดูความต่อเนื่องเธรด"
|
||||
deleteAccountConfirm: "การดำเนินการนี้จะลบบัญชีของคุณอย่างถาวรเลยนะ แน่ใจหรอดำเนินการ?"
|
||||
deleteAccountConfirm: "บัญชีจะถูกลบ ดำเนินการต่อใช่ไหม?"
|
||||
incorrectPassword: "รหัสผ่านไม่ถูกต้อง"
|
||||
incorrectTotp: "รหัสยืนยันตัวตนแบบใช้ครั้งเดียวที่ท่านได้ระบุมานั้น ไม่ถูกต้องหรือหมดอายุลงแล้วค่ะ"
|
||||
voteConfirm: "ต้องการโหวต “{choice}” ใช่ไหม?"
|
||||
@@ -991,7 +998,7 @@ pleaseSelect: "ตัวเลือก"
|
||||
reverse: "พลิก"
|
||||
colored: "สี"
|
||||
refreshInterval: "ความถี่ในการอัปเดต"
|
||||
label: "ป้ายชื่อ"
|
||||
label: "ป้าย"
|
||||
type: "รูปแบบ"
|
||||
speed: "ความเร็ว"
|
||||
slow: "ช้า"
|
||||
@@ -1019,6 +1026,9 @@ pushNotificationAlreadySubscribed: "การแจ้งเตือนแบ
|
||||
pushNotificationNotSupported: "เบราว์เซอร์หรือเซิร์ฟเวอร์ไม่รองรับการแจ้งเตือนแบบพุช"
|
||||
sendPushNotificationReadMessage: "ลบการแจ้งเตือนแบบพุชเมื่ออ่านการแจ้งเตือนหรือข้อความที่เกี่ยวข้องแล้ว"
|
||||
sendPushNotificationReadMessageCaption: "อาจทำให้อุปกรณ์ของคุณใช้พลังงานมากขึ้น"
|
||||
pleaseAllowPushNotification: "โปรดอนุญาตการตั้งค่าการแจ้งเตือนของเบราว์เซอร์"
|
||||
browserPushNotificationDisabled: "ขอสิทธิ์ส่งการแจ้งเตือนล้มเหลว"
|
||||
browserPushNotificationDisabledDescription: "ไม่มีสิทธิ์ในการส่งการแจ้งเตือนจาก {serverName} โปรดอนุญาตการแจ้งเตือนในตั้งค่าของเบราว์เซอร์ แล้วลองอีกครั้ง"
|
||||
windowMaximize: "ขยายใหญ่สุด"
|
||||
windowMinimize: "ย่อเล็กที่สุด"
|
||||
windowRestore: "เลิกทำ"
|
||||
@@ -1099,8 +1109,8 @@ license: "ใบอนุญาต"
|
||||
unfavoriteConfirm: "ลบออกจากรายการโปรดแน่ใจหรอ?"
|
||||
myClips: "คลิปของฉัน"
|
||||
drivecleaner: "ทำความสะอาดไดรฟ์"
|
||||
retryAllQueuesNow: "ลองเรียกใช้คิวทั้งหมดอีกครั้ง"
|
||||
retryAllQueuesConfirmTitle: "ลองใหม่ทั้งหมดจริงๆหรอแน่ใจนะ?"
|
||||
retryAllQueuesNow: "ลองใหม่ทุกคิวทันที"
|
||||
retryAllQueuesConfirmTitle: "ลองใหม่ทันทีเลยไหม?"
|
||||
retryAllQueuesConfirmText: "สิ่งนี้จะเพิ่มการโหลดเซิร์ฟเวอร์ชั่วคราวนะ"
|
||||
enableChartsForRemoteUser: "สร้างแผนภูมิข้อมูลผู้ใช้ระยะไกล"
|
||||
enableChartsForFederatedInstances: "สร้างแผนภูมิของเซิร์ฟเวอร์ระยะไกล"
|
||||
@@ -1150,7 +1160,7 @@ initialAccountSetting: "ตั้งค่าโปรไฟล์"
|
||||
youFollowing: "ติดตามแล้ว"
|
||||
preventAiLearning: "ปฏิเสธการเรียนรู้ด้วย generative AI"
|
||||
preventAiLearningDescription: "ส่งคำร้องขอไม่ให้ใช้ ข้อความในโน้ตที่โพสต์, หรือเนื้อหารูปภาพ ฯลฯ ในการเรียนรู้ของเครื่อง(machine learning) / Predictive AI / Generative AI โดยการเพิ่มแฟล็ก “noai” ลง HTML-Response ให้กับเนื้อหาที่เกี่ยวข้อง แต่ทั้งนี้ ไม่ได้ป้องกัน AI จากการเรียนรู้ได้อย่างสมบูรณ์ เนื่องจากมี AI บางตัวเท่านั้นที่จะเคารพคำขอดังกล่าว"
|
||||
options: "ตัวเลือกบทบาท"
|
||||
options: "ตัวเลือก"
|
||||
specifyUser: "ผู้ใช้เฉพาะ"
|
||||
lookupConfirm: "ต้องการเรียกดูข้อมูลใช่ไหม?"
|
||||
openTagPageConfirm: "ต้องการเปิดหน้าแฮชแท็กใช่ไหม?"
|
||||
@@ -1169,6 +1179,7 @@ installed: "ติดตั้งแล้ว"
|
||||
branding: "แบรนดิ้ง"
|
||||
enableServerMachineStats: "เผยแพร่สถานะฮาร์ดแวร์ของเซิร์ฟเวอร์"
|
||||
enableIdenticonGeneration: "เปิดใช้งานผู้ใช้สร้างตัวระบุ"
|
||||
showRoleBadgesOfRemoteUsers: "แสดงตราบทบาทที่มอบให้กับผู้ใช้ระยะไกล"
|
||||
turnOffToImprovePerformance: "การปิดส่วนนี้สามารถเพิ่มประสิทธิภาพได้"
|
||||
createInviteCode: "สร้างรหัสเชิญ"
|
||||
createWithOptions: "สร้างด้วยตัวเลือก"
|
||||
@@ -1247,7 +1258,7 @@ refreshing: "กำลังรีเฟรช..."
|
||||
pullDownToRefresh: "ดึงลงเพื่อรีเฟรช"
|
||||
useGroupedNotifications: "แสดงผลการแจ้งเตือนแบบกลุ่มแล้ว"
|
||||
emailVerificationFailedError: "เกิดปัญหาในขณะตรวจสอบอีเมล อาจเป็นไปได้ว่าลิงก์หมดอายุแล้ว"
|
||||
cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุคำอธิบาย"
|
||||
cwNotationRequired: "หากเปิดใช้งาน “ซ่อนเนื้อหา” จะต้องระบุข้อความเกริ่น"
|
||||
doReaction: "เพิ่มรีแอคชั่น"
|
||||
code: "โค้ด"
|
||||
reloadRequiredToApplySettings: "จำเป็นต้องมีการโหลดซ้ำเพื่อให้การตั้งค่ามีผล"
|
||||
@@ -1390,17 +1401,53 @@ scheduledToPostOnX: "มีการกำหนดเวลาให้โพ
|
||||
schedule: "กำหนดเวลา"
|
||||
scheduled: "กำหนดเวลา"
|
||||
widgets: "วิดเจ็ต"
|
||||
deviceInfo: "รายละเอียดอุปกรณ์"
|
||||
deviceInfoDescription: "เมื่อต้องการรับความช่วยเหลือทางเทคนิค กรุณาระบุข้อมูลต่อไปนี้ซึ่งอาจช่วยแก้ไขปัญหาได้"
|
||||
youAreAdmin: "คุณคือผู้ดูแลระบบ"
|
||||
frame: "เฟรม"
|
||||
presets: "พรีเซ็ต"
|
||||
zeroPadding: "ห่างเป็น 0"
|
||||
nothingToConfigure: "ไม่มีอะไรให้ต้ังค่า"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "แคปชั่นของไฟล์"
|
||||
filename: "ชื่อไฟล์"
|
||||
filename_without_ext: "ชื่อไฟล์ที่ไม่มีนามสกุล"
|
||||
year: "ปีที่ถ่าย"
|
||||
month: "เดือนที่ถ่าย"
|
||||
day: "วันที่ถ่าย"
|
||||
hour: "เวลาที่ถ่าย (ชั่วโมง)"
|
||||
minute: "เวลาที่ถ่าย (นาที)"
|
||||
second: "เวลาที่ถ่าย (วินาที)"
|
||||
camera_model: "ชื่อกล้อง"
|
||||
camera_lens_model: "ชื่อเลนส์"
|
||||
camera_mm: "ความยาวโฟกัส"
|
||||
camera_mm_35: "ทางยาวโฟกัส (เทียบเท่า 35 มม.)"
|
||||
camera_f: "รูรับแสง"
|
||||
camera_s: "ความเร็วชัตเตอร์"
|
||||
camera_iso: "ความไวแสง ISO"
|
||||
gps_lat: "ละติจูด"
|
||||
gps_long: "ลองจิจูด"
|
||||
_imageFrameEditor:
|
||||
title: "แก้ไขเฟรม"
|
||||
tip: "สามารถตกแต่งภาพโดยการเพิ่มป้ายที่มีเฟรมหรือเมทาเดต้าได้"
|
||||
header: "ส่วนหัว"
|
||||
footer: "ท้ายกระดาษ"
|
||||
borderThickness: "ความกว้างขอบ"
|
||||
labelThickness: "ความกว้างป้าย"
|
||||
labelScale: "สเกลของป้าย"
|
||||
centered: "จัดกึ่งกลาง"
|
||||
captionMain: "แคปชั่น (ใหญ่)"
|
||||
captionSub: "แคปชั่น (เล็ก)"
|
||||
availableVariables: "ตัวแปรที่สามารถใช้ได้"
|
||||
withQrCode: "QR โค้ด"
|
||||
backgroundColor: "สีพื้นหลัง"
|
||||
textColor: "สีตัวอักษร"
|
||||
font: "แบบอักษร"
|
||||
fontSerif: "Serif"
|
||||
fontSansSerif: "Sans Serif"
|
||||
quitWithoutSaveConfirm: "ต้องการออกโดยไม่บันทึกหรือไม่?"
|
||||
failedToLoadImage: "โหลดภาพล้มเหลว"
|
||||
_compression:
|
||||
_quality:
|
||||
high: "คุณภาพสูง"
|
||||
@@ -1503,6 +1550,11 @@ _settings:
|
||||
showUrlPreview: "แสดงตัวอย่าง URL"
|
||||
showAvailableReactionsFirstInNote: "แสดงรีแอคชั่นที่ใช้ได้ไว้หน้าสุด"
|
||||
showPageTabBarBottom: "แสดงแท็บบาร์ของเพจที่ด้านล่าง"
|
||||
emojiPaletteBanner: "สามารถบันทึกพรีเซ็ตเป็นจานสีเพื่อตรึงไว้ในตัวจิ้มเอโมจิ หรือปรับแต่งวิธีการแสดงผลของตัวจิ้มเอโมจิได้"
|
||||
enableAnimatedImages: "เปิดใช้งานภาพเคลื่อนไหว"
|
||||
settingsPersistence_title: "คงสภาพการตั้งค่า"
|
||||
settingsPersistence_description1: "เมื่อเปิดใช้งานการคงสภาพการตั้งค่า จะช่วยป้องกันไม่ให้ข้อมูลการตั้งค่าสูญหายได้"
|
||||
settingsPersistence_description2: "แต่ในบางสภาพแวดล้อม อาจไม่สามารถเปิดใช้งานได้"
|
||||
_chat:
|
||||
showSenderName: "แสดงชื่อผู้ส่ง"
|
||||
sendOnEnter: "กด Enter เพื่อส่ง"
|
||||
@@ -1511,6 +1563,8 @@ _preferencesProfile:
|
||||
profileNameDescription: "กรุณาตั้งชื่อเพื่อระบุอุปกรณ์นี้"
|
||||
profileNameDescription2: "เช่น: “คอมเครื่องหลัก”, “มือถือ” ฯลฯ"
|
||||
manageProfiles: "จัดการโปรไฟล์"
|
||||
shareSameProfileBetweenDevicesIsNotRecommended: "ไม่แนะนำให้ใช้โปรไฟล์เดียวกันร่วมกันบนหลายอุปกรณ์"
|
||||
useSyncBetweenDevicesOptionIfYouWantToSyncSetting: "หากมีรายการตั้งค่าที่ต้องการซิงก์ระหว่างหลายอุปกรณ์ โปรดเปิดใช้งานตัวเลือก “ซิงก์ระหว่างหลายอุปกรณ์” ในอุปกรณ์นั้นๆ ด้วย"
|
||||
_preferencesBackup:
|
||||
autoBackup: "สำรองโดยอัตโนมัติ"
|
||||
restoreFromBackup: "คืนค่าจากข้อมูลสำรอง"
|
||||
@@ -1518,8 +1572,9 @@ _preferencesBackup:
|
||||
noBackupsFoundDescription: "ไม่พบข้อมูลสำรองที่สร้างโดยอัตโนมัติ แต่หากมีข้อมูลสำรองที่บันทึกด้วยตนเอง สามารถนำเข้ามาเพื่อกู้คืนได้"
|
||||
selectBackupToRestore: "กรุณาเลือกข้อมูลสำรองที่ต้องการกู้คืน"
|
||||
youNeedToNameYourProfileToEnableAutoBackup: "จำเป็นต้องตั้งชื่อโปรไฟล์ก่อนจึงจะเปิดใช้งานการสำรองข้อมูลอัตโนมัติได้"
|
||||
autoPreferencesBackupIsNotEnabledForThisDevice: "ยังไม่ได้เปิดใช้งานการสำรองข้อมูลอัตโนมัติบนอุปกรณ์นี้"
|
||||
autoPreferencesBackupIsNotEnabledForThisDevice: "ยังไม่ได้เปิดใช้งานการสำรองการตั้งค่าแบบอัตโนมัติบนอุปกรณ์นี้"
|
||||
backupFound: "พบข้อมูลสำรองของการตั้งค่าแล้ว"
|
||||
forceBackup: "บังคับสำรองการตั้งค่า"
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "ต้องเข้าสู่ระบบเพื่อดูเนื้อหา"
|
||||
requireSigninToViewContentsDescription1: "กำหนดให้ต้องเข้าสู่ระบบก่อนจึงจะสามารถดูโน้ตหรือเนื้อหาทั้งหมดที่สร้างไว้ได้ ซึ่งช่วยป้องกันไม่ให้ข้อมูลถูกเก็บโดยบอตหรือ Crawler (โปรแกรมรวบรวมข้อมูล)"
|
||||
@@ -1587,7 +1642,7 @@ _initialAccountSetting:
|
||||
theseSettingsCanEditLater: "คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ในภายหลังได้ตลอดเวลานะ"
|
||||
youCanEditMoreSettingsInSettingsPageLater: "สามารถตั้งค่าเพิ่มเติมได้ที่หน้า “การตั้งค่า” อย่าลืมไปเยี่ยมชมภายหลังด้วย"
|
||||
followUsers: "ลองติดตามผู้ใช้ที่สนใจเพื่อสร้างไทม์ไลน์ดูสิ"
|
||||
pushNotificationDescription: "กำลังเปิดใช้งานการแจ้งเตือนแบบพุชจะช่วยให้คุณได้รับการแจ้งเตือนจาก {name} โดยตรงบนอุปกรณ์ของคุณนะ"
|
||||
pushNotificationDescription: "เมื่อเปิดใช้งานการแจ้งเตือนแบบพุช จะสามารถรับการแจ้งเตือนจาก {name} บนอุปกรณ์ที่ใช้งานอยู่ได้"
|
||||
initialAccountSettingCompleted: "ตั้งค่าโปรไฟล์เสร็จสมบูรณ์แล้ว!"
|
||||
haveFun: "ขอให้สนุกกับ {name}!"
|
||||
youCanContinueTutorial: "คุณสามารถดำเนินการต่อด้วยบทช่วยสอนเกี่ยวกับวิธีใช้ {name} (Misskey) หรือออกจากบทช่วยสอนแล้วเริ่มใช้งานได้ทันที"
|
||||
@@ -1639,7 +1694,7 @@ _initialTutorial:
|
||||
localOnly: "การโพสต์ด้วย flag นี้จะไม่รวมโน้ตไปยังเซิร์ฟเวอร์อื่น ผู้ใช้บนเซิร์ฟเวอร์อื่นจะไม่สามารถดูโน้ตเหล่านี้ได้โดยตรง โดยไม่คำนึงถึงการตั้งค่าการแสดงผลข้างต้น"
|
||||
_cw:
|
||||
title: "คำเตือนเกี่ยวกับเนื้อหา"
|
||||
description: "เนื้อหาที่เขียนใน “คำอธิบายประกอบ” จะแสดงแทนเนื้อหาหลัก ต้องคลิก “ดูเพิ่มเติม” เพื่อให้เนื้อหาหลักแสดง"
|
||||
description: "เนื้อหาที่เขียนใน “ข้อความเกริ่น” จะแสดงแทนเนื้อหาหลัก ต้องกด “ดูเพิ่มเติม” เพื่อให้เนื้อหาหลักแสดง"
|
||||
_exampleNote:
|
||||
cw: " ห้ามดู ระวังหิว"
|
||||
note: "เพิ่งไปกินโดนัทเคลือบช็อคโกแลตมา 🍩😋"
|
||||
@@ -1991,7 +2046,7 @@ _role:
|
||||
isConditionalRole: "นี่คือบทบาทที่มีเงื่อนไข"
|
||||
isPublic: "ทำให้บทบาทเปิดเผยต่อสาธารณะ"
|
||||
descriptionOfIsPublic: "บทบาทจะปรากฏบนโปรไฟล์ของผู้ใช้และเปิดเผยต่อสาธารณะ (ทุกคนสามารถเห็นได้ว่าผู้ใช้รายนี้มีบทบาทนี้)"
|
||||
options: "ตัวเลือกบทบาท"
|
||||
options: "ตัวเลือก"
|
||||
policies: "นโยบาย"
|
||||
baseRole: "แม่แบบบทบาท"
|
||||
useBaseValue: "ใช้ตามแม่แบบบทบาท"
|
||||
@@ -2025,6 +2080,7 @@ _role:
|
||||
canManageAvatarDecorations: "จัดการตกแต่งอวตาร"
|
||||
driveCapacity: "ความจุของไดรฟ์"
|
||||
maxFileSize: "ขนาดไฟล์สูงสุดที่สามารถอัปโหลดได้"
|
||||
maxFileSize_caption: "รีเวิร์สพร็อกซี, CDN และคอมโพเนนต์หน้าบ้านอื่นๆ อาจมีค่าการตั้งค่าของตนเอง"
|
||||
alwaysMarkNsfw: "ทำเครื่องหมายไฟล์ว่าเป็น NSFW เสมอ"
|
||||
canUpdateBioMedia: "อนุญาตให้เปลี่ยนไอคอนประจำตัวและแบนเนอร์"
|
||||
pinMax: "จํานวนสูงสุดของโน้ตที่ปักหมุดไว้"
|
||||
@@ -2386,7 +2442,7 @@ _permissions:
|
||||
"read:admin:index-stats": "ดูข้อมูลเกี่ยวกับดัชนีฐานข้อมูล"
|
||||
"read:admin:table-stats": "ดูข้อมูลเกี่ยวกับตารางในฐานข้อมูล"
|
||||
"read:admin:user-ips": "ดูที่อยู่ IP ของผู้ใช้"
|
||||
"read:admin:meta": "ดูข้อมูลอภิพันธุ์ของอินสแตนซ์"
|
||||
"read:admin:meta": "ดูเมทาเดต้าของอินสแตนซ์"
|
||||
"write:admin:reset-password": "รีเซ็ตรหัสผ่านของผู้ใช้"
|
||||
"write:admin:resolve-abuse-user-report": "แก้ไขรายงานจากผู้ใช้"
|
||||
"write:admin:send-email": "ส่งอีเมล"
|
||||
@@ -2397,7 +2453,7 @@ _permissions:
|
||||
"write:admin:unset-user-avatar": "ลบอวตารผู้ใช้"
|
||||
"write:admin:unset-user-banner": "ลบแบนเนอร์ผู้ใช้"
|
||||
"write:admin:unsuspend-user": "ยกเลิกการระงับผู้ใช้"
|
||||
"write:admin:meta": "จัดการข้อมูลอภิพันธุ์ของอินสแตนซ์"
|
||||
"write:admin:meta": "จัดการเมทาเดต้าของอินสแตนซ์"
|
||||
"write:admin:user-note": "จัดการโน้ตการกลั่นกรอง"
|
||||
"write:admin:roles": "จัดการบทบาท"
|
||||
"read:admin:roles": "ดูบทบาท"
|
||||
@@ -2443,6 +2499,7 @@ _auth:
|
||||
scopeUser: "กำลังดำเนินการในฐานะผู้ใช้ต่อไปนี้"
|
||||
pleaseLogin: "กรุณาเข้าสู่ระบบเพื่ออนุมัติแอปพลิเคชัน"
|
||||
byClickingYouWillBeRedirectedToThisUrl: "หากอนุญาตการเข้าถึง ระบบจะเปลี่ยนเส้นทางไปยัง URL ด้านล่างโดยอัตโนมัติ"
|
||||
alreadyAuthorized: "แอปพลิเคชันนี้ได้รับอนุญาตให้เข้าถึงแล้ว"
|
||||
_antennaSources:
|
||||
all: "โน้ตทั้งหมด"
|
||||
homeTimeline: "โน้ตจากผู้ใช้ที่ติดตาม"
|
||||
@@ -2491,11 +2548,40 @@ _widgets:
|
||||
chat: "แชตเลย"
|
||||
_widgetOptions:
|
||||
showHeader: "แสดงส่วนหัว"
|
||||
transparent: "ทำพื้นหลังโปรงใส"
|
||||
height: "ความสูง"
|
||||
_button:
|
||||
colored: "สี"
|
||||
_clock:
|
||||
size: "ขนาด"
|
||||
thickness: "ความหนาเข็ม"
|
||||
thicknessThin: "บาง"
|
||||
thicknessMedium: "ปานกลาง"
|
||||
thicknessThick: "หนา"
|
||||
graduations: "ขีดบอกค่าบนหน้าปัด"
|
||||
graduationDots: "จุด"
|
||||
graduationArabic: "เลขอารบิก"
|
||||
fadeGraduations: "เฟดหน้าปัด"
|
||||
sAnimation: "การเคลื่อนไหวของเข็มวินาที"
|
||||
sAnimationElastic: "สมจริง"
|
||||
sAnimationEaseOut: "ลื่นๆ"
|
||||
twentyFour: "ระบบ 24 ชั่วโมง"
|
||||
labelTime: "เวลา"
|
||||
labelTz: "เขตเวลา"
|
||||
labelTimeAndTz: "เวลาและเขตเวลา"
|
||||
timezone: "เขตเวลา"
|
||||
showMs: "แสดงมิลลิวินาที"
|
||||
showLabel: "แสดงป้าย"
|
||||
_jobQueue:
|
||||
sound: "เล่นเสียง"
|
||||
_rss:
|
||||
url: "URL ของฟีด RSS"
|
||||
refreshIntervalSec: "ห้วงอัปเดต (วินาที)"
|
||||
maxEntries: "จำนวนที่แสดงได้สูงสุด"
|
||||
_rssTicker:
|
||||
shuffle: "สุ่มลำดับ"
|
||||
duration: "ความเร็วทิกเกอร์ (วินาที)"
|
||||
reverse: "วิ่งไปอีกทาง"
|
||||
_birthdayFollowings:
|
||||
period: "ระยะเวลา"
|
||||
_cw:
|
||||
@@ -2542,9 +2628,20 @@ _postForm:
|
||||
replyPlaceholder: "ตอบกลับโน้ตนี้..."
|
||||
quotePlaceholder: "อ้างโน้ตนี้..."
|
||||
channelPlaceholder: "โพสต์ลงช่อง..."
|
||||
showHowToUse: "แสดงวิธีใช้ฟอร์ม"
|
||||
_howToUse:
|
||||
content_title: "เนื้อความ"
|
||||
content_description: "ป้อนเนื้อหาที่จะโพสต์"
|
||||
toolbar_title: "แถบเครื่องมือ"
|
||||
toolbar_description: "สามารถแนบไฟล์หรือแบบสอบถาม ตั้งข้อความเกริ่นหรือแฮชแท็ก แทรกเอโมจิหรือการกล่าวถึง เป็นต้น"
|
||||
account_title: "เมนูบัญชี"
|
||||
account_description: "สามารถสลับบัญชีที่ใช้โพสต์ หรือดูรายการฉบับร่างและโพสต์กำหนดเวลาไว้ซึ่งบันทึกไว้ในบัญชีได้"
|
||||
visibility_title: "การมองเห็น"
|
||||
visibility_description: "สามารถตั้งค่าขอบเขตการเผยแพร่โน้ตได้"
|
||||
menu_title: "เมนู"
|
||||
menu_description: "สามารถบันทึกเป็นฉบับร่าง ตั้งเวลาการโพสต์ ตั้งค่ารีแอคชั่น และดำเนินการอื่นๆ ได้"
|
||||
submit_title: "ปุ่มโพสต์"
|
||||
submit_description: "กดปุ่มนั้นเพื่อโพสต์โน้ต หรือกด Ctrl + Enter / Cmd + Return เพื่อโพสต์ก็ได้เช่นกัน"
|
||||
_placeholders:
|
||||
a: "ตอนนี้เป็นยังไงบ้าง?"
|
||||
b: "มีอะไรเกิดขึ้นหรือเปล่า?"
|
||||
@@ -2560,7 +2657,7 @@ _profile:
|
||||
metadata: "ข้อมูลเพิ่มเติม"
|
||||
metadataEdit: "แก้ไขข้อมูลเพิ่มเติม"
|
||||
metadataDescription: "ใช้สิ่งเหล่านี้ คุณสามารถแสดงฟิลด์ข้อมูลเพิ่มเติมในโปรไฟล์ของคุณ"
|
||||
metadataLabel: "ป้ายชื่อ"
|
||||
metadataLabel: "ป้าย"
|
||||
metadataContent: "เนื้อหา"
|
||||
changeAvatar: "เปลี่ยนไอคอนประจำตัว"
|
||||
changeBanner: "เปลี่ยนแบนเนอร์"
|
||||
@@ -2721,6 +2818,8 @@ _notification:
|
||||
quote: "อ้างอิง"
|
||||
reaction: "รีแอคชั่น"
|
||||
pollEnded: "โพลสิ้นสุดแล้ว"
|
||||
scheduledNotePosted: "โพสต์กำหนดเวลาสำเร็จ"
|
||||
scheduledNotePostFailed: "โพสต์กำหนดเวลาล้มเหลว"
|
||||
receiveFollowRequest: "ได้รับคำร้องขอติดตาม"
|
||||
followRequestAccepted: "อนุมัติให้ติดตามแล้ว"
|
||||
roleAssigned: "ให้บทบาท"
|
||||
@@ -2760,6 +2859,14 @@ _deck:
|
||||
usedAsMinWidthWhenFlexible: "ความกว้างขั้นต่ำนั้นจะถูกใช้งานสำหรับสิ่งนี้เมื่อเปิดใช้งานตัวเลือก \"ปรับความกว้างอัตโนมัติ\" หากเลือกเปิดใช้งานแล้ว"
|
||||
flexible: "ปรับความกว้างอัตโนมัติ"
|
||||
enableSyncBetweenDevicesForProfiles: "เปิดใช้งานการซิงค์ข้อมูลโปรไฟล์ระหว่างอุปกรณ์"
|
||||
showHowToUse: "แสดงวิธีใช้ UI"
|
||||
_howToUse:
|
||||
addColumn_title: "เพิ่มคอลัมน์"
|
||||
addColumn_description: "สามารถเลือกประเภทของคอลัมน์แล้วเพิ่มได้"
|
||||
settings_title: "ตั้งค่า UI"
|
||||
settings_description: "สามารถตั้งค่ารายละเอียดของ UI แบบเด็คได้"
|
||||
switchProfile_title: "สลับโปรไฟล์"
|
||||
switchProfile_description: "สามารถบันทึกเลย์เอาต์ของ UI เป็นโปรไฟล์ และสลับใช้งานได้ทุกเมื่อ"
|
||||
_columns:
|
||||
main: "หลัก"
|
||||
widgets: "วิดเจ็ต"
|
||||
@@ -2820,6 +2927,8 @@ _abuseReport:
|
||||
notifiedWebhook: "Webhook ที่ใช้"
|
||||
deleteConfirm: "ต้องการลบปลายทางการแจ้งเตือนใช่ไหม?"
|
||||
_moderationLogTypes:
|
||||
clearQueue: "ล้างคิว"
|
||||
promoteQueue: "ดันงานในคิวอีกครั้ง"
|
||||
createRole: "สร้างบทบาทแล้ว"
|
||||
deleteRole: "ลบบทบาทแล้ว"
|
||||
updateRole: "อัปเดตบทบาทแล้ว"
|
||||
@@ -3063,7 +3172,7 @@ _customEmojisManager:
|
||||
uploadSettingDescription: "สามารถกำหนดพฤติกรรมขณะอัปโหลดเอโมจิจากหน้าจอนี้ได้"
|
||||
directoryToCategoryLabel: "ป้อนชื่อไดเรกทอรีเป็น \"category\""
|
||||
directoryToCategoryCaption: "เมื่อทำการลากและวางไดเรกทอรี ชื่อจะถูกป้อนเป็น \"category\""
|
||||
confirmRegisterEmojisDescription: "จะลงทะเบียนเอโมจิที่แสดงในรายการเป็นเอโมจิแบบกำหนดเองใหม่\nดำเนินการต่อหรือไม่? (เพื่อหลีกเลี่ยงภาระโหลดหนัก ระบบจะสามารถลงทะเบียนอีโมจิได้สูงสุด {count} รายการต่อครั้ง)"
|
||||
confirmRegisterEmojisDescription: "จะลงทะเบียนเอโมจิที่แสดงในรายการเป็นเอโมจิแบบกำหนดเองใหม่\nดำเนินการต่อหรือไม่? (เพื่อหลีกเลี่ยงภาระโหลดหนัก ระบบจะสามารถลงทะเบียนเอโมจิได้สูงสุด {count} รายการต่อครั้ง)"
|
||||
confirmClearEmojisDescription: "ต้องการยกเลิกการแก้ไขและล้างรายการเอโมจิที่แสดงอยู่หรือไม่?"
|
||||
confirmUploadEmojisDescription: "จะอัปโหลดไฟล์ {count} รายการที่ลากและวางไปยังไดรฟ์ ดำเนินการหรือไม่?"
|
||||
_embedCodeGen:
|
||||
@@ -3214,6 +3323,7 @@ _watermarkEditor:
|
||||
title: "แก้ไขลายน้ำ"
|
||||
cover: "ซ้อนทับทั่วทั้งพื้นที่"
|
||||
repeat: "ปูให้เต็มพื้นที่"
|
||||
preserveBoundingRect: "ปรับไม่ให้ล้นขอบเมื่อหมุน"
|
||||
opacity: "ความทึบแสง"
|
||||
scale: "ขนาด"
|
||||
text: "ข้อความ"
|
||||
@@ -3235,10 +3345,12 @@ _watermarkEditor:
|
||||
polkadotSubDotRadius: "ขนาดของจุดรอง"
|
||||
polkadotSubDotDivisions: "จำนวนจุดรอง"
|
||||
leaveBlankToAccountUrl: "เว้นว่างไว้หากต้องการใช้ URL ของบัญชีแทน"
|
||||
failedToLoadImage: "โหลดภาพล้มเหลว"
|
||||
_imageEffector:
|
||||
title: "เอฟเฟกต์"
|
||||
addEffect: "เพิ่มเอฟเฟกต์"
|
||||
discardChangesConfirm: "ต้องการทิ้งการเปลี่ยนแปลงแล้วออกหรือไม่?"
|
||||
failedToLoadImage: "โหลดภาพล้มเหลว"
|
||||
_fxs:
|
||||
chromaticAberration: "ความคลาดสี"
|
||||
glitch: "กลิตช์"
|
||||
|
||||
+4
-3
@@ -164,7 +164,7 @@ selectAntenna: "选择天线"
|
||||
editAntenna: "编辑天线"
|
||||
createAntenna: "创建天线"
|
||||
selectWidget: "选择小工具"
|
||||
editWidgets: "编辑部件"
|
||||
editWidgets: "编辑小工具"
|
||||
editWidgetsExit: "完成编辑"
|
||||
customEmojis: "自定义表情符号"
|
||||
emoji: "表情符号"
|
||||
@@ -543,6 +543,7 @@ regenerate: "重新生成"
|
||||
fontSize: "字体大小"
|
||||
mediaListWithOneImageAppearance: "仅一张图片的媒体列表高度"
|
||||
limitTo: "上限为 {x}"
|
||||
showMediaListByGridInWideArea: "在大屏幕上并排显示媒体列表"
|
||||
noFollowRequests: "没有关注请求"
|
||||
openImageInNewTab: "在新标签页中打开图片"
|
||||
dashboard: "管理面板"
|
||||
@@ -831,7 +832,7 @@ youAreRunningUpToDateClient: "您所使用的客户端已经是最新的。"
|
||||
newVersionOfClientAvailable: "新版本的客户端可用。"
|
||||
usageAmount: "使用量"
|
||||
capacity: "容量"
|
||||
inUse: "使用中"
|
||||
inUse: "已使用"
|
||||
editCode: "编辑代码"
|
||||
apply: "应用"
|
||||
receiveAnnouncementFromInstance: "从服务器接收通知"
|
||||
@@ -2255,7 +2256,7 @@ _menuDisplay:
|
||||
top: "顶部"
|
||||
hide: "隐藏"
|
||||
_wordMute:
|
||||
muteWords: "要屏蔽的词"
|
||||
muteWords: "要折叠的词"
|
||||
muteWordsDescription: "AND 条件用空格分隔,OR 条件用换行符分隔。"
|
||||
muteWordsDescription2: "正则表达式用斜线包裹"
|
||||
_instanceMute:
|
||||
|
||||
@@ -543,6 +543,7 @@ regenerate: "再次生成"
|
||||
fontSize: "字體大小"
|
||||
mediaListWithOneImageAppearance: "只有一張圖片時的檔案列表高度"
|
||||
limitTo: "上限為 {x}"
|
||||
showMediaListByGridInWideArea: "當畫面寬度較寬時,將媒體清單以橫向排列顯示"
|
||||
noFollowRequests: "沒有追隨您的請求"
|
||||
openImageInNewTab: "於新分頁中開啟圖片"
|
||||
dashboard: "儀表板"
|
||||
|
||||
+14
-12
@@ -6,7 +6,7 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"workspaces": [
|
||||
"packages/misskey-js",
|
||||
"packages/i18n",
|
||||
@@ -52,6 +52,10 @@
|
||||
"clean-all": "node scripts/clean-all.mjs",
|
||||
"cleanall": "pnpm clean-all"
|
||||
},
|
||||
"resolutions": {
|
||||
"chokidar": "5.0.0",
|
||||
"lodash": "4.17.21"
|
||||
},
|
||||
"dependencies": {
|
||||
"cssnano": "7.1.2",
|
||||
"esbuild": "0.27.2",
|
||||
@@ -59,23 +63,23 @@
|
||||
"ignore-walk": "8.0.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"postcss": "8.5.6",
|
||||
"tar": "7.5.6",
|
||||
"terser": "5.46.0"
|
||||
"tar": "7.5.2",
|
||||
"terser": "5.44.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.2",
|
||||
"@misskey-dev/eslint-plugin": "2.2.0",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "24.10.9",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260116.1",
|
||||
"@types/node": "24.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251226.1",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.9.0",
|
||||
"cypress": "15.8.1",
|
||||
"eslint": "9.39.2",
|
||||
"globals": "16.5.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "10.28.2",
|
||||
"pnpm": "10.27.0",
|
||||
"typescript": "5.9.3",
|
||||
"start-server-and-test": "2.1.3"
|
||||
},
|
||||
@@ -84,9 +88,7 @@
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@aiscript-dev/aiscript-languageserver": "-",
|
||||
"chokidar": "5.0.0",
|
||||
"lodash": "4.17.23"
|
||||
"@aiscript-dev/aiscript-languageserver": "-"
|
||||
},
|
||||
"ignoredBuiltDependencies": [
|
||||
"@sentry-internal/node-cpu-profiler",
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class SpReactions1769664628306 {
|
||||
name = 'SpReactions1769664628306'
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TABLE "note_sp_reaction" ("id" character varying(32) NOT NULL, "userId" character varying(32) NOT NULL, "noteId" character varying(32) NOT NULL, "reaction" character varying(260) NOT NULL, "noteUserId" character varying(32) NOT NULL, CONSTRAINT "PK_11fd5ecdd9bb91517edfcf890d9" PRIMARY KEY ("id")); COMMENT ON COLUMN "note_sp_reaction"."noteUserId" IS '[Denormalized]'`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_3463a48b09fa41e1826ebd9f58" ON "note_sp_reaction" ("userId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_bfe4caa46cc0526bc2932d6dbe" ON "note_sp_reaction" ("noteId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_8be6eb3f4edc9940a3f8142669" ON "note_sp_reaction" ("noteUserId") `);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_b5f210b20bd987fe8584c85d33" ON "note_sp_reaction" ("userId", "noteId") `);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "enableSpReaction" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "note_sp_reaction" ADD CONSTRAINT "FK_3463a48b09fa41e1826ebd9f585" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "note_sp_reaction" ADD CONSTRAINT "FK_bfe4caa46cc0526bc2932d6dbed" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {QueryRunner} queryRunner
|
||||
*/
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "note_sp_reaction" DROP CONSTRAINT "FK_bfe4caa46cc0526bc2932d6dbed"`);
|
||||
await queryRunner.query(`ALTER TABLE "note_sp_reaction" DROP CONSTRAINT "FK_3463a48b09fa41e1826ebd9f585"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableSpReaction"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_b5f210b20bd987fe8584c85d33"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_8be6eb3f4edc9940a3f8142669"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_bfe4caa46cc0526bc2932d6dbe"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_3463a48b09fa41e1826ebd9f58"`);
|
||||
await queryRunner.query(`DROP TABLE "note_sp_reaction"`);
|
||||
}
|
||||
}
|
||||
@@ -41,17 +41,17 @@
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@swc/core-darwin-arm64": "1.15.8",
|
||||
"@swc/core-darwin-x64": "1.15.8",
|
||||
"@swc/core-darwin-arm64": "1.15.7",
|
||||
"@swc/core-darwin-x64": "1.15.7",
|
||||
"@swc/core-freebsd-x64": "1.3.11",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.8",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.8",
|
||||
"@swc/core-linux-arm64-musl": "1.15.8",
|
||||
"@swc/core-linux-x64-gnu": "1.15.8",
|
||||
"@swc/core-linux-x64-musl": "1.15.8",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.8",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.8",
|
||||
"@swc/core-win32-x64-msvc": "1.15.8",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.7",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.7",
|
||||
"@swc/core-linux-arm64-musl": "1.15.7",
|
||||
"@swc/core-linux-x64-gnu": "1.15.7",
|
||||
"@swc/core-linux-x64-musl": "1.15.7",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.7",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.7",
|
||||
"@swc/core-win32-x64-msvc": "1.15.7",
|
||||
"@tensorflow/tfjs": "4.22.0",
|
||||
"@tensorflow/tfjs-node": "4.22.0",
|
||||
"bufferutil": "4.1.0",
|
||||
@@ -71,39 +71,40 @@
|
||||
"utf-8-validate": "6.0.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.970.0",
|
||||
"@aws-sdk/lib-storage": "3.970.0",
|
||||
"@aws-sdk/client-s3": "3.958.0",
|
||||
"@aws-sdk/lib-storage": "3.958.0",
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"@fastify/accepts": "5.0.4",
|
||||
"@fastify/cors": "11.2.0",
|
||||
"@fastify/express": "4.0.4",
|
||||
"@fastify/express": "4.0.2",
|
||||
"@fastify/http-proxy": "11.4.1",
|
||||
"@fastify/multipart": "9.3.0",
|
||||
"@fastify/static": "8.3.0",
|
||||
"@kitajs/html": "4.2.11",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
"@napi-rs/canvas": "0.1.88",
|
||||
"@nestjs/common": "11.1.12",
|
||||
"@nestjs/core": "11.1.12",
|
||||
"@nestjs/testing": "11.1.12",
|
||||
"@napi-rs/canvas": "0.1.87",
|
||||
"@nestjs/common": "11.1.10",
|
||||
"@nestjs/core": "11.1.10",
|
||||
"@nestjs/testing": "11.1.10",
|
||||
"@peertube/http-signature": "1.7.0",
|
||||
"@sentry/node": "10.34.0",
|
||||
"@sentry/profiling-node": "10.34.0",
|
||||
"@sentry/node": "10.32.1",
|
||||
"@sentry/profiling-node": "10.32.1",
|
||||
"@simplewebauthn/server": "13.2.2",
|
||||
"@sinonjs/fake-timers": "15.1.0",
|
||||
"@smithy/node-http-handler": "4.4.8",
|
||||
"@swc/cli": "0.7.10",
|
||||
"@swc/core": "1.15.8",
|
||||
"@smithy/node-http-handler": "4.4.7",
|
||||
"@swc/cli": "0.7.9",
|
||||
"@swc/core": "1.15.7",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@types/redis-info": "3.0.3",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.17.1",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"bcryptjs": "3.0.3",
|
||||
"blurhash": "2.0.5",
|
||||
"body-parser": "2.2.2",
|
||||
"bullmq": "5.66.5",
|
||||
"body-parser": "2.2.1",
|
||||
"bullmq": "5.66.3",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"chalk": "5.6.2",
|
||||
"chalk-template": "1.1.2",
|
||||
@@ -112,24 +113,24 @@
|
||||
"content-disposition": "1.0.1",
|
||||
"date-fns": "4.1.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"fastify": "5.7.1",
|
||||
"fastify": "5.6.2",
|
||||
"fastify-raw-body": "5.0.0",
|
||||
"feed": "5.2.0",
|
||||
"file-type": "21.3.0",
|
||||
"feed": "5.1.0",
|
||||
"file-type": "21.2.0",
|
||||
"fluent-ffmpeg": "2.1.3",
|
||||
"form-data": "4.0.5",
|
||||
"got": "14.6.6",
|
||||
"got": "14.6.5",
|
||||
"hpagent": "1.2.0",
|
||||
"http-link-header": "1.1.3",
|
||||
"i18n": "workspace:*",
|
||||
"ioredis": "5.9.2",
|
||||
"ioredis": "5.8.2",
|
||||
"ip-cidr": "4.0.2",
|
||||
"ipaddr.js": "2.3.0",
|
||||
"is-svg": "6.1.0",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "9.0.0",
|
||||
"juice": "11.1.0",
|
||||
"meilisearch": "0.55.0",
|
||||
"juice": "11.0.3",
|
||||
"meilisearch": "0.54.0",
|
||||
"mfm-js": "0.25.0",
|
||||
"mime-types": "3.0.2",
|
||||
"misskey-js": "workspace:*",
|
||||
@@ -138,14 +139,14 @@
|
||||
"nanoid": "5.1.6",
|
||||
"nested-property": "4.0.0",
|
||||
"node-fetch": "3.3.2",
|
||||
"node-html-parser": "7.0.2",
|
||||
"node-html-parser": "7.0.1",
|
||||
"nodemailer": "7.0.12",
|
||||
"nsfwjs": "4.2.0",
|
||||
"oauth2orize": "1.12.0",
|
||||
"oauth2orize-pkce": "0.1.2",
|
||||
"os-utils": "0.0.14",
|
||||
"otpauth": "9.4.1",
|
||||
"pg": "8.17.1",
|
||||
"pg": "8.16.3",
|
||||
"pkce-challenge": "5.0.1",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
@@ -153,6 +154,7 @@
|
||||
"random-seed": "0.3.0",
|
||||
"ratelimiter": "3.4.1",
|
||||
"re2": "1.23.0",
|
||||
"redis-info": "3.1.0",
|
||||
"reflect-metadata": "0.2.2",
|
||||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
@@ -164,7 +166,7 @@
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"systeminformation": "5.30.5",
|
||||
"systeminformation": "5.28.1",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.5",
|
||||
"tsc-alias": "1.8.16",
|
||||
@@ -172,14 +174,14 @@
|
||||
"ulid": "3.0.2",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
"ws": "8.19.0",
|
||||
"ws": "8.18.3",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.7.0",
|
||||
"@kitajs/ts-html-plugin": "4.1.3",
|
||||
"@nestjs/platform-express": "11.1.12",
|
||||
"@sentry/vue": "10.34.0",
|
||||
"@nestjs/platform-express": "11.1.10",
|
||||
"@sentry/vue": "10.32.1",
|
||||
"@simplewebauthn/types": "12.0.0",
|
||||
"@swc/jest": "0.2.39",
|
||||
"@types/accepts": "1.3.7",
|
||||
@@ -193,8 +195,8 @@
|
||||
"@types/jsonld": "1.5.15",
|
||||
"@types/mime-types": "3.0.1",
|
||||
"@types/ms": "2.1.0",
|
||||
"@types/node": "24.10.9",
|
||||
"@types/nodemailer": "7.0.5",
|
||||
"@types/node": "24.10.4",
|
||||
"@types/nodemailer": "7.0.4",
|
||||
"@types/oauth2orize": "1.11.5",
|
||||
"@types/oauth2orize-pkce": "0.1.2",
|
||||
"@types/pg": "8.16.0",
|
||||
@@ -212,22 +214,22 @@
|
||||
"@types/vary": "1.1.3",
|
||||
"@types/web-push": "3.6.4",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cbor": "10.0.11",
|
||||
"cross-env": "10.1.0",
|
||||
"esbuild-plugin-swc": "1.0.1",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"execa": "9.6.1",
|
||||
"fkill": "10.0.3",
|
||||
"fkill": "10.0.1",
|
||||
"jest": "29.7.0",
|
||||
"jest-mock": "29.7.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"nodemon": "3.1.11",
|
||||
"pid-port": "2.0.1",
|
||||
"pid-port": "2.0.0",
|
||||
"simple-oauth2": "5.1.0",
|
||||
"supertest": "7.2.2",
|
||||
"vite": "7.3.1"
|
||||
"supertest": "7.1.4",
|
||||
"vite": "7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,56 +14,24 @@ import { fork } from 'node:child_process';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import * as http from 'node:http';
|
||||
import * as fs from 'node:fs/promises';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const SAMPLE_COUNT = 3; // Number of samples to measure
|
||||
const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup
|
||||
const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle
|
||||
|
||||
const keys = {
|
||||
VmPeak: 0,
|
||||
VmSize: 0,
|
||||
VmHWM: 0,
|
||||
VmRSS: 0,
|
||||
VmData: 0,
|
||||
VmStk: 0,
|
||||
VmExe: 0,
|
||||
VmLib: 0,
|
||||
VmPTE: 0,
|
||||
VmSwap: 0,
|
||||
};
|
||||
|
||||
async function getMemoryUsage(pid) {
|
||||
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
|
||||
|
||||
const result = {};
|
||||
for (const key of Object.keys(keys)) {
|
||||
const match = status.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`));
|
||||
if (match) {
|
||||
result[key] = parseInt(match[1], 10);
|
||||
} else {
|
||||
throw new Error(`Failed to parse ${key} from /proc/${pid}/status`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function measureMemory() {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Start the Misskey backend server using fork to enable IPC
|
||||
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), ['expose-gc'], {
|
||||
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), [], {
|
||||
cwd: join(__dirname, '..'),
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'production',
|
||||
MK_DISABLE_CLUSTERING: '1',
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||
execArgv: [...process.execArgv, '--expose-gc'],
|
||||
});
|
||||
|
||||
let serverReady = false;
|
||||
@@ -89,40 +57,6 @@ async function measureMemory() {
|
||||
process.stderr.write(`[server error] ${err}\n`);
|
||||
});
|
||||
|
||||
async function triggerGc() {
|
||||
const ok = new Promise((resolve) => {
|
||||
serverProcess.once('message', (message) => {
|
||||
if (message === 'gc ok') resolve();
|
||||
});
|
||||
});
|
||||
|
||||
serverProcess.send('gc');
|
||||
|
||||
await ok;
|
||||
|
||||
await setTimeout(1000);
|
||||
}
|
||||
|
||||
function createRequest() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request({
|
||||
host: 'localhost',
|
||||
port: 61812,
|
||||
path: '/api/meta',
|
||||
method: 'POST',
|
||||
}, (res) => {
|
||||
res.on('data', () => { });
|
||||
res.on('end', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
req.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for server to be ready or timeout
|
||||
const startupStartTime = Date.now();
|
||||
while (!serverReady) {
|
||||
@@ -139,23 +73,46 @@ async function measureMemory() {
|
||||
// Wait for memory to settle
|
||||
await setTimeout(MEMORY_SETTLE_TIME);
|
||||
|
||||
// Get memory usage from the server process via /proc
|
||||
const pid = serverProcess.pid;
|
||||
let memoryInfo;
|
||||
|
||||
const beforeGc = await getMemoryUsage(pid);
|
||||
try {
|
||||
const fs = await import('node:fs/promises');
|
||||
|
||||
await triggerGc();
|
||||
// Read /proc/[pid]/status for detailed memory info
|
||||
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
|
||||
const vmRssMatch = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
||||
const vmDataMatch = status.match(/VmData:\s+(\d+)\s+kB/);
|
||||
const vmSizeMatch = status.match(/VmSize:\s+(\d+)\s+kB/);
|
||||
|
||||
const afterGc = await getMemoryUsage(pid);
|
||||
memoryInfo = {
|
||||
rss: vmRssMatch ? parseInt(vmRssMatch[1], 10) * 1024 : null,
|
||||
heapUsed: vmDataMatch ? parseInt(vmDataMatch[1], 10) * 1024 : null,
|
||||
vmSize: vmSizeMatch ? parseInt(vmSizeMatch[1], 10) * 1024 : null,
|
||||
};
|
||||
} catch (err) {
|
||||
// Fallback: use ps command
|
||||
process.stderr.write(`Warning: Could not read /proc/${pid}/status: ${err}\n`);
|
||||
|
||||
// create some http requests to simulate load
|
||||
const REQUEST_COUNT = 10;
|
||||
await Promise.all(
|
||||
Array.from({ length: REQUEST_COUNT }).map(() => createRequest()),
|
||||
);
|
||||
|
||||
await triggerGc();
|
||||
|
||||
const afterRequest = await getMemoryUsage(pid);
|
||||
const { execSync } = await import('node:child_process');
|
||||
try {
|
||||
const ps = execSync(`ps -o rss= -p ${pid}`, { encoding: 'utf-8' });
|
||||
const rssKb = parseInt(ps.trim(), 10);
|
||||
memoryInfo = {
|
||||
rss: rssKb * 1024,
|
||||
heapUsed: null,
|
||||
vmSize: null,
|
||||
};
|
||||
} catch {
|
||||
memoryInfo = {
|
||||
rss: null,
|
||||
heapUsed: null,
|
||||
vmSize: null,
|
||||
error: 'Could not measure memory',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the server
|
||||
serverProcess.kill('SIGTERM');
|
||||
@@ -178,51 +135,15 @@ async function measureMemory() {
|
||||
|
||||
const result = {
|
||||
timestamp: new Date().toISOString(),
|
||||
beforeGc,
|
||||
afterGc,
|
||||
afterRequest,
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 直列の方が時間的に分散されて正確そうだから直列でやる
|
||||
const results = [];
|
||||
for (let i = 0; i < SAMPLE_COUNT; i++) {
|
||||
const res = await measureMemory();
|
||||
results.push(res);
|
||||
}
|
||||
|
||||
// Calculate averages
|
||||
const beforeGc = structuredClone(keys);
|
||||
const afterGc = structuredClone(keys);
|
||||
const afterRequest = structuredClone(keys);
|
||||
for (const res of results) {
|
||||
for (const key of Object.keys(keys)) {
|
||||
beforeGc[key] += res.beforeGc[key];
|
||||
afterGc[key] += res.afterGc[key];
|
||||
afterRequest[key] += res.afterRequest[key];
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(keys)) {
|
||||
beforeGc[key] = Math.round(beforeGc[key] / SAMPLE_COUNT);
|
||||
afterGc[key] = Math.round(afterGc[key] / SAMPLE_COUNT);
|
||||
afterRequest[key] = Math.round(afterRequest[key] / SAMPLE_COUNT);
|
||||
}
|
||||
|
||||
const result = {
|
||||
timestamp: new Date().toISOString(),
|
||||
beforeGc,
|
||||
afterGc,
|
||||
afterRequest,
|
||||
startupTimeMs: startupTime,
|
||||
memory: memoryInfo,
|
||||
};
|
||||
|
||||
// Output as JSON to stdout
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
measureMemory().catch((err) => {
|
||||
console.error(JSON.stringify({
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
@@ -86,18 +86,6 @@ if (!envOption.disableClustering) {
|
||||
ev.mount();
|
||||
}
|
||||
|
||||
process.on('message', msg => {
|
||||
if (msg === 'gc') {
|
||||
if (global.gc != null) {
|
||||
logger.info('Manual GC triggered');
|
||||
global.gc();
|
||||
if (process.send != null) process.send('gc ok');
|
||||
} else {
|
||||
logger.warn('Manual GC requested but gc is not available. Start the process with --expose-gc to enable this feature.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
readyRef.value = true;
|
||||
|
||||
// ユニットテスト時にMisskeyが子プロセスで起動された時のため
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MetricsTime, type JobType } from 'bullmq';
|
||||
import { parse as parseRedisInfo } from 'redis-info';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
|
||||
@@ -85,19 +86,6 @@ const REPEATABLE_SYSTEM_JOB_DEF = [{
|
||||
pattern: '0 4 * * *',
|
||||
}];
|
||||
|
||||
function parseRedisInfo(infoText: string): Record<string, string> {
|
||||
const fields = infoText
|
||||
.split('\n')
|
||||
.filter(line => line.length > 0 && !line.startsWith('#'))
|
||||
.map(line => line.trim().split(':'));
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
for (const [key, value] of fields) {
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
constructor(
|
||||
@@ -902,7 +890,7 @@ export class QueueService {
|
||||
},
|
||||
db: {
|
||||
version: db.redis_version,
|
||||
mode: db.redis_mode as 'cluster' | 'standalone' | 'sentinel',
|
||||
mode: db.redis_mode,
|
||||
runId: db.run_id,
|
||||
processId: db.process_id,
|
||||
port: parseInt(db.tcp_port),
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta, MiNoteSpReaction, NoteSpReactionsRepository } from '@/models/_.js';
|
||||
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta } from '@/models/_.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
@@ -74,9 +73,6 @@ export class ReactionService {
|
||||
@Inject(DI.meta)
|
||||
private meta: MiMeta,
|
||||
|
||||
@Inject(DI.redis)
|
||||
private redisClient: Redis.Redis,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@@ -86,9 +82,6 @@ export class ReactionService {
|
||||
@Inject(DI.noteReactionsRepository)
|
||||
private noteReactionsRepository: NoteReactionsRepository,
|
||||
|
||||
@Inject(DI.noteSpReactionsRepository)
|
||||
private noteSpReactionsRepository: NoteSpReactionsRepository,
|
||||
|
||||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
@@ -344,118 +337,6 @@ export class ReactionService {
|
||||
//#endregion
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createSp(user: { id: MiUser['id']; isBot: MiUser['isBot'] }, note: MiNote, reaction: string) {
|
||||
if (!this.meta.enableSpReaction) {
|
||||
throw new IdentifiableError('52c432ea-b166-491c-a73b-5dd703221b20');
|
||||
}
|
||||
|
||||
if (note.userId === user.id) {
|
||||
throw new IdentifiableError('afa694bf-6661-4d72-b8f7-bfb86a7545a1');
|
||||
}
|
||||
|
||||
if (note.userHost !== null) {
|
||||
throw new IdentifiableError('b59abda2-0d81-49e3-8148-80cf35ac4402');
|
||||
}
|
||||
|
||||
if (note.reactionAcceptance === 'likeOnly') {
|
||||
throw new IdentifiableError('1b168811-a1aa-470a-9b61-7fcf807cf9c1');
|
||||
}
|
||||
|
||||
if (!await this.noteEntityService.isVisibleForMe(note, user.id)) {
|
||||
throw new IdentifiableError('3ce0e3bc-7d48-4e87-a902-578c6ffd369e', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
// monthly limit
|
||||
const policies = await this.roleService.getUserPolicies(user.id);
|
||||
if (policies.spReactionsMonthlyLimit === 0) {
|
||||
throw new IdentifiableError('e371be02-9478-4133-90ef-8401ee38e474');
|
||||
}
|
||||
const month = new Date().getUTCMonth() + 1;
|
||||
const monthlySpReactionsCountMapKey = `monthlySpReactionsCountMap:${user.id}:${month}`;
|
||||
const count = await this.redisClient.get(monthlySpReactionsCountMapKey);
|
||||
if (count != null && parseInt(count, 10) >= policies.spReactionsMonthlyLimit) {
|
||||
throw new IdentifiableError('82e1a10c-52a8-4ccb-8ff7-3678bff68444');
|
||||
}
|
||||
|
||||
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
||||
if (blocked) {
|
||||
throw new IdentifiableError('388ee683-8720-4aea-9ac8-b8c92d260815');
|
||||
}
|
||||
|
||||
const custom = reaction.match(isCustomEmojiRegexp);
|
||||
if (custom) {
|
||||
const name = custom[1];
|
||||
const emoji = (await this.customEmojiService.localEmojisCache.fetch()).get(name);
|
||||
|
||||
if (emoji == null) {
|
||||
throw new IdentifiableError('47c098e2-d0b6-4197-8d00-5a68bbb156be');
|
||||
}
|
||||
|
||||
// センシティブ
|
||||
if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) {
|
||||
throw new IdentifiableError('7fc2efbd-2652-4a60-975b-6eb65f60c7b3');
|
||||
}
|
||||
|
||||
if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 && !(await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) {
|
||||
// リアクションとして使う権限がない
|
||||
throw new IdentifiableError('63288e20-4251-4c62-a9d5-9da4e0bdd41e');
|
||||
}
|
||||
|
||||
reaction = `:${name}:`;
|
||||
} else {
|
||||
reaction = this.normalize(reaction);
|
||||
}
|
||||
|
||||
const record: MiNoteSpReaction = {
|
||||
id: this.idService.gen(),
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
reaction,
|
||||
noteUserId: note.userId,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.noteSpReactionsRepository.insert(record);
|
||||
} catch (e) {
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
throw new IdentifiableError('c9e8b0d0-d532-4453-8cc1-5cf8e95ba764');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// increment monthly reactions count
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.incr(monthlySpReactionsCountMapKey);
|
||||
redisPipeline.expireat(monthlySpReactionsCountMapKey,
|
||||
(Date.now() / 1000) + (60 * 60 * 24 * 40), // TTLは最低でも一か月存続しさえすれば厳密でなくていい
|
||||
'NX',
|
||||
);
|
||||
redisPipeline.exec();
|
||||
|
||||
// 3日以内に投稿されたノートの場合ハイライト用ランキング更新
|
||||
if (
|
||||
(Date.now() - this.idService.parse(note.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3
|
||||
) {
|
||||
if (note.channelId != null) {
|
||||
if (note.replyId == null) {
|
||||
this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1);
|
||||
}
|
||||
} else {
|
||||
if (note.visibility === 'public' && note.replyId == null) {
|
||||
this.featuredService.updateGlobalNotesRanking(note.id, 1);
|
||||
this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.notificationService.createNotification(note.userId, 'spReaction', {
|
||||
noteId: note.id,
|
||||
reaction: reaction,
|
||||
}, user.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* - 文字列タイプのレガシーな形式のリアクションを現在の形式に変換する
|
||||
* - ローカルのリアクションのホストを `@.` にする(`decodeReaction()`の効果)
|
||||
|
||||
@@ -71,7 +71,6 @@ export type RolePolicies = {
|
||||
noteDraftLimit: number;
|
||||
scheduledNoteLimit: number;
|
||||
watermarkAvailable: boolean;
|
||||
spReactionsMonthlyLimit: number;
|
||||
};
|
||||
|
||||
export const DEFAULT_POLICIES: RolePolicies = {
|
||||
@@ -119,7 +118,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
noteDraftLimit: 10,
|
||||
scheduledNoteLimit: 1,
|
||||
watermarkAvailable: true,
|
||||
spReactionsMonthlyLimit: 0,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -29,7 +29,6 @@ const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set([
|
||||
'renote:grouped',
|
||||
'quote',
|
||||
'reaction',
|
||||
'spReaction',
|
||||
'reaction:grouped',
|
||||
'pollEnded',
|
||||
'scheduledNotePosted',
|
||||
|
||||
@@ -14,10 +14,6 @@ import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
function assertBw(bw: string): bw is Packed<'ReversiGameDetailed'>['bw'] {
|
||||
return ['random', '1', '2'].includes(bw);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReversiGameEntityService {
|
||||
constructor(
|
||||
@@ -62,7 +58,7 @@ export class ReversiGameEntityService {
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
black: game.black,
|
||||
bw: assertBw(game.bw) ? game.bw : 'random',
|
||||
bw: game.bw,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
@@ -120,7 +116,7 @@ export class ReversiGameEntityService {
|
||||
surrenderedUserId: game.surrenderedUserId,
|
||||
timeoutUserId: game.timeoutUserId,
|
||||
black: game.black,
|
||||
bw: assertBw(game.bw) ? game.bw : 'random',
|
||||
bw: game.bw,
|
||||
isLlotheo: game.isLlotheo,
|
||||
canPutEverywhere: game.canPutEverywhere,
|
||||
loopedBoard: game.loopedBoard,
|
||||
|
||||
@@ -24,7 +24,6 @@ export const DI = {
|
||||
noteFavoritesRepository: Symbol('noteFavoritesRepository'),
|
||||
noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'),
|
||||
noteReactionsRepository: Symbol('noteReactionsRepository'),
|
||||
noteSpReactionsRepository: Symbol('noteSpReactionsRepository'),
|
||||
pollsRepository: Symbol('pollsRepository'),
|
||||
pollVotesRepository: Symbol('pollVotesRepository'),
|
||||
userProfilesRepository: Symbol('userProfilesRepository'),
|
||||
|
||||
@@ -64,7 +64,6 @@ import {
|
||||
packedMetaDetailedOnlySchema,
|
||||
packedMetaDetailedSchema,
|
||||
packedMetaLiteSchema,
|
||||
packedMetaClientOptionsSchema,
|
||||
} from '@/models/json-schema/meta.js';
|
||||
import { packedUserWebhookSchema } from '@/models/json-schema/user-webhook.js';
|
||||
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
||||
@@ -136,7 +135,6 @@ export const refs = {
|
||||
MetaLite: packedMetaLiteSchema,
|
||||
MetaDetailedOnly: packedMetaDetailedOnlySchema,
|
||||
MetaDetailed: packedMetaDetailedSchema,
|
||||
MetaClientOptions: packedMetaClientOptionsSchema,
|
||||
UserWebhook: packedUserWebhookSchema,
|
||||
SystemWebhook: packedSystemWebhookSchema,
|
||||
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
||||
|
||||
@@ -722,19 +722,10 @@ export class MiMeta {
|
||||
})
|
||||
public showRoleBadgesOfRemoteUsers: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableSpReaction: boolean;
|
||||
|
||||
@Column('jsonb', {
|
||||
default: { },
|
||||
})
|
||||
public clientOptions: {
|
||||
entrancePageStyle: 'classic' | 'simple';
|
||||
showTimelineForVisitor: boolean;
|
||||
showActivitiesForVisitor: boolean;
|
||||
};
|
||||
public clientOptions: Record<string, any>;
|
||||
}
|
||||
|
||||
export type SoftwareSuspension = {
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
import { MiNote } from './Note.js';
|
||||
|
||||
@Entity('note_sp_reaction')
|
||||
@Index(['userId', 'noteId'], { unique: true })
|
||||
export class MiNoteSpReaction {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public userId: MiUser['id'];
|
||||
|
||||
@ManyToOne(() => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user?: MiUser | null;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public noteId: MiNote['id'];
|
||||
|
||||
@ManyToOne(() => MiNote, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public note?: MiNote | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 260,
|
||||
})
|
||||
public reaction: string;
|
||||
|
||||
//#region Denormalized fields
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: '[Denormalized]',
|
||||
})
|
||||
public noteUserId: MiUser['id'];
|
||||
//#endregion
|
||||
}
|
||||
@@ -55,13 +55,6 @@ export type MiNotification = {
|
||||
notifierId: MiUser['id'];
|
||||
noteId: MiNote['id'];
|
||||
reaction: string;
|
||||
} | {
|
||||
type: 'spReaction';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
notifierId: MiUser['id'];
|
||||
noteId: MiNote['id'];
|
||||
reaction: string;
|
||||
} | {
|
||||
type: 'pollEnded';
|
||||
id: string;
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
MiNote,
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteSpReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiNoteDraft,
|
||||
MiPage,
|
||||
@@ -143,12 +142,6 @@ const $noteReactionsRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $noteSpReactionsRepository: Provider = {
|
||||
provide: DI.noteSpReactionsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiNoteSpReaction).extend(miRepository as MiRepository<MiNoteSpReaction>),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $noteDraftsRepository: Provider = {
|
||||
provide: DI.noteDraftsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiNoteDraft).extend(miRepository as MiRepository<MiNoteDraft>),
|
||||
@@ -563,7 +556,6 @@ const $reversiGamesRepository: Provider = {
|
||||
$noteFavoritesRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
$noteSpReactionsRepository,
|
||||
$noteDraftsRepository,
|
||||
$pollsRepository,
|
||||
$pollVotesRepository,
|
||||
@@ -642,7 +634,6 @@ const $reversiGamesRepository: Provider = {
|
||||
$noteFavoritesRepository,
|
||||
$noteThreadMutingsRepository,
|
||||
$noteReactionsRepository,
|
||||
$noteSpReactionsRepository,
|
||||
$noteDraftsRepository,
|
||||
$pollsRepository,
|
||||
$pollVotesRepository,
|
||||
|
||||
@@ -23,7 +23,7 @@ import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
|
||||
import { MiChannel } from '@/models/Channel.js';
|
||||
import { MiChannelFavorite } from '@/models/ChannelFavorite.js';
|
||||
import { MiChannelFollowing } from '@/models/ChannelFollowing.js';
|
||||
import { MiChannelMuting } from '@/models/ChannelMuting.js';
|
||||
import { MiChannelMuting } from "@/models/ChannelMuting.js";
|
||||
import { MiChatApproval } from '@/models/ChatApproval.js';
|
||||
import { MiChatMessage } from '@/models/ChatMessage.js';
|
||||
import { MiChatRoom } from '@/models/ChatRoom.js';
|
||||
@@ -50,7 +50,6 @@ import { MiNote } from '@/models/Note.js';
|
||||
import { MiNoteDraft } from '@/models/NoteDraft.js';
|
||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
import { MiNoteSpReaction } from '@/models/NoteSpReaction.js';
|
||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
||||
import { MiPage } from '@/models/Page.js';
|
||||
import { MiPageLike } from '@/models/PageLike.js';
|
||||
@@ -132,7 +131,6 @@ export {
|
||||
MiNoteDraft,
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteSpReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiPage,
|
||||
MiPageLike,
|
||||
@@ -213,7 +211,6 @@ export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>;
|
||||
export type NoteDraftsRepository = Repository<MiNoteDraft> & MiRepository<MiNoteDraft>;
|
||||
export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>;
|
||||
export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>;
|
||||
export type NoteSpReactionsRepository = Repository<MiNoteSpReaction> & MiRepository<MiNoteSpReaction>;
|
||||
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>;
|
||||
export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>;
|
||||
export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>;
|
||||
|
||||
@@ -72,7 +72,8 @@ export const packedMetaLiteSchema = {
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
clientOptions: {
|
||||
ref: 'MetaClientOptions',
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
disableRegistration: {
|
||||
type: 'boolean',
|
||||
@@ -396,23 +397,3 @@ export const packedMetaDetailedSchema = {
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
export const packedMetaClientOptionsSchema = {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
entrancePageStyle: {
|
||||
type: 'string',
|
||||
enum: ['classic', 'simple'],
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
showTimelineForVisitor: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
showActivitiesForVisitor: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -182,35 +182,6 @@ export const packedNotificationSchema = {
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...baseSchema.properties,
|
||||
type: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['spReaction'],
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
ref: 'UserLite',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
},
|
||||
note: {
|
||||
type: 'object',
|
||||
ref: 'Note',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
reaction: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
||||
@@ -81,7 +81,6 @@ export const packedReversiGameLiteSchema = {
|
||||
bw: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['random', '1', '2'],
|
||||
},
|
||||
noIrregularRules: {
|
||||
type: 'boolean',
|
||||
@@ -200,7 +199,6 @@ export const packedReversiGameDetailedSchema = {
|
||||
bw: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['random', '1', '2'],
|
||||
},
|
||||
noIrregularRules: {
|
||||
type: 'boolean',
|
||||
|
||||
@@ -608,7 +608,6 @@ export const packedMeDetailedOnlySchema = {
|
||||
renote: { optional: true, ...notificationRecieveConfig },
|
||||
quote: { optional: true, ...notificationRecieveConfig },
|
||||
reaction: { optional: true, ...notificationRecieveConfig },
|
||||
spReaction: { optional: true, ...notificationRecieveConfig },
|
||||
pollEnded: { optional: true, ...notificationRecieveConfig },
|
||||
scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
|
||||
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
|
||||
|
||||
@@ -44,7 +44,6 @@ import { MiRenoteMuting } from '@/models/RenoteMuting.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import { MiNoteFavorite } from '@/models/NoteFavorite.js';
|
||||
import { MiNoteReaction } from '@/models/NoteReaction.js';
|
||||
import { MiNoteSpReaction } from '@/models/NoteSpReaction.js';
|
||||
import { MiNoteThreadMuting } from '@/models/NoteThreadMuting.js';
|
||||
import { MiNoteDraft } from '@/models/NoteDraft.js';
|
||||
import { MiPage } from '@/models/Page.js';
|
||||
@@ -205,7 +204,6 @@ export const entities = [
|
||||
MiNote,
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteSpReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiNoteDraft,
|
||||
MiPage,
|
||||
|
||||
@@ -428,7 +428,8 @@ export const meta = {
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
clientOptions: {
|
||||
ref: 'MetaClientOptions',
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
@@ -596,10 +597,6 @@ export const meta = {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
enableSpReaction: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
@@ -756,7 +753,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes,
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes,
|
||||
showRoleBadgesOfRemoteUsers: instance.showRoleBadgesOfRemoteUsers,
|
||||
enableSpReaction: instance.enableSpReaction,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable, Inject } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
@@ -68,14 +67,7 @@ export const paramDef = {
|
||||
description: { type: 'string', nullable: true },
|
||||
defaultLightTheme: { type: 'string', nullable: true },
|
||||
defaultDarkTheme: { type: 'string', nullable: true },
|
||||
clientOptions: {
|
||||
type: 'object', nullable: false,
|
||||
properties: {
|
||||
entrancePageStyle: { type: 'string', nullable: false, enum: ['classic', 'simple'] },
|
||||
showTimelineForVisitor: { type: 'boolean', nullable: false },
|
||||
showActivitiesForVisitor: { type: 'boolean', nullable: false },
|
||||
},
|
||||
},
|
||||
clientOptions: { type: 'object', nullable: false },
|
||||
cacheRemoteFiles: { type: 'boolean' },
|
||||
cacheRemoteSensitiveFiles: { type: 'boolean' },
|
||||
emailRequiredForSignup: { type: 'boolean' },
|
||||
@@ -218,7 +210,6 @@ export const paramDef = {
|
||||
remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' },
|
||||
remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' },
|
||||
showRoleBadgesOfRemoteUsers: { type: 'boolean' },
|
||||
enableSpReaction: { type: 'boolean' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
@@ -226,9 +217,6 @@ export const paramDef = {
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private serverSettings: MiMeta,
|
||||
|
||||
private metaService: MetaService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
@@ -341,10 +329,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
}
|
||||
|
||||
if (ps.clientOptions !== undefined) {
|
||||
set.clientOptions = {
|
||||
...serverSettings.clientOptions,
|
||||
...ps.clientOptions,
|
||||
};
|
||||
set.clientOptions = ps.clientOptions;
|
||||
}
|
||||
|
||||
if (ps.cacheRemoteFiles !== undefined) {
|
||||
@@ -763,10 +748,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
set.showRoleBadgesOfRemoteUsers = ps.showRoleBadgesOfRemoteUsers;
|
||||
}
|
||||
|
||||
if (ps.enableSpReaction !== undefined) {
|
||||
set.enableSpReaction = ps.enableSpReaction;
|
||||
}
|
||||
|
||||
const before = await this.metaService.fetch(true);
|
||||
|
||||
await this.metaService.update(set);
|
||||
|
||||
@@ -208,7 +208,6 @@ export const paramDef = {
|
||||
renote: notificationRecieveConfig,
|
||||
quote: notificationRecieveConfig,
|
||||
reaction: notificationRecieveConfig,
|
||||
spReaction: notificationRecieveConfig,
|
||||
pollEnded: notificationRecieveConfig,
|
||||
scheduledNotePosted: notificationRecieveConfig,
|
||||
scheduledNotePostFailed: notificationRecieveConfig,
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
* renote - 投稿がRenoteされた
|
||||
* quote - 投稿が引用Renoteされた
|
||||
* reaction - 投稿にリアクションされた
|
||||
* spReaction - スペシャルリアクションされた
|
||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||
* scheduledNotePosted - 予約したノートが投稿された
|
||||
* scheduledNotePostFailed - 予約したノートの投稿に失敗した
|
||||
@@ -34,7 +33,6 @@ export const notificationTypes = [
|
||||
'renote',
|
||||
'quote',
|
||||
'reaction',
|
||||
'spReaction',
|
||||
'pollEnded',
|
||||
'scheduledNotePosted',
|
||||
'scheduledNotePostFailed',
|
||||
|
||||
@@ -11,15 +11,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/node": "24.10.9",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"rollup": "4.55.1"
|
||||
"@types/node": "24.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"rollup": "4.54.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"estree-walker": "3.0.3",
|
||||
"magic-string": "0.30.21",
|
||||
"vite": "7.3.1"
|
||||
"vite": "7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,15 +144,7 @@ export default [
|
||||
'vue/return-in-computed-property': 'warn',
|
||||
'vue/no-setup-props-reactivity-loss': 'warn',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/html-self-closing': ['error', {
|
||||
html: {
|
||||
void: 'any',
|
||||
normal: 'never',
|
||||
component: 'any',
|
||||
},
|
||||
svg: 'any',
|
||||
math: 'any',
|
||||
}],
|
||||
'vue/html-self-closing': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/v-on-event-hyphenation': ['error', 'never', {
|
||||
autofix: true,
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
"mfm-js": "0.25.0",
|
||||
"misskey-js": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.55.1",
|
||||
"sass": "1.97.2",
|
||||
"shiki": "3.21.0",
|
||||
"rollup": "4.54.0",
|
||||
"sass": "1.97.1",
|
||||
"shiki": "3.20.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"uuid": "13.0.0",
|
||||
"vite": "7.3.1",
|
||||
"vite": "7.3.0",
|
||||
"vue": "3.5.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -39,29 +39,29 @@
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/micromatch": "4.0.10",
|
||||
"@types/node": "24.10.9",
|
||||
"@types/node": "24.10.4",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@vitest/coverage-v8": "4.0.17",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"@vue/runtime-core": "3.5.26",
|
||||
"acorn": "8.15.0",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.7.0",
|
||||
"happy-dom": "20.3.1",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"happy-dom": "20.0.11",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.12.7",
|
||||
"msw": "2.12.6",
|
||||
"nodemon": "3.1.11",
|
||||
"prettier": "3.8.0",
|
||||
"prettier": "3.7.4",
|
||||
"start-server-and-test": "2.1.3",
|
||||
"tsx": "4.21.0",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vue-component-type-helpers": "3.2.2",
|
||||
"vue-component-type-helpers": "3.2.1",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.2.2"
|
||||
"vue-tsc": "3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="user.isCat" :class="[$style.ears]">
|
||||
<div :class="$style.earLeft">
|
||||
<div v-if="false" :class="$style.layer">
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.earRight">
|
||||
<div v-if="false" :class="$style.layer">
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
|
||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"></canvas>
|
||||
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
|
||||
<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.10.9",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@types/node": "24.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"esbuild": "0.27.2",
|
||||
"eslint-plugin-vue": "10.7.0",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"nodemon": "3.1.11",
|
||||
"vue-eslint-parser": "10.2.0"
|
||||
},
|
||||
|
||||
@@ -147,15 +147,7 @@ export default [
|
||||
'vue/return-in-computed-property': 'warn',
|
||||
'vue/no-setup-props-reactivity-loss': 'warn',
|
||||
'vue/max-attributes-per-line': 'off',
|
||||
'vue/html-self-closing': ['error', {
|
||||
html: {
|
||||
void: 'any',
|
||||
normal: 'never',
|
||||
component: 'any',
|
||||
},
|
||||
svg: 'any',
|
||||
math: 'any',
|
||||
}],
|
||||
'vue/html-self-closing': 'off',
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/v-on-event-hyphenation': ['error', 'never', {
|
||||
autofix: true,
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
"@analytics/google-analytics": "1.1.0",
|
||||
"@discordapp/twemoji": "16.0.1",
|
||||
"@github/webauthn-json": "2.1.1",
|
||||
"@mcaptcha/core-glue": "0.1.0-alpha-5",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-rc2",
|
||||
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "6.0.3",
|
||||
"@rollup/pluginutils": "5.3.0",
|
||||
"@sentry/vue": "10.34.0",
|
||||
"@sentry/vue": "10.32.1",
|
||||
"@syuilo/aiscript": "1.2.1",
|
||||
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
@@ -39,13 +39,13 @@
|
||||
"chartjs-chart-matrix": "3.0.0",
|
||||
"chartjs-plugin-gradient": "0.6.1",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"chromatic": "13.3.5",
|
||||
"chromatic": "13.3.4",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "2.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"eventemitter3": "5.0.1",
|
||||
"execa": "9.6.1",
|
||||
"exifreader": "4.36.0",
|
||||
"exifreader": "4.33.1",
|
||||
"frontend-shared": "workspace:*",
|
||||
"i18n": "workspace:*",
|
||||
"icons-subsetter": "workspace:*",
|
||||
@@ -55,7 +55,7 @@
|
||||
"is-file-animated": "1.0.2",
|
||||
"json5": "2.2.3",
|
||||
"matter-js": "0.20.0",
|
||||
"mediabunny": "1.28.0",
|
||||
"mediabunny": "1.27.2",
|
||||
"mfm-js": "0.25.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
@@ -64,16 +64,16 @@
|
||||
"punycode.js": "2.3.1",
|
||||
"qr-code-styling": "1.9.2",
|
||||
"qr-scanner": "1.4.2",
|
||||
"rollup": "4.55.1",
|
||||
"rollup": "4.54.0",
|
||||
"sanitize-html": "2.17.0",
|
||||
"sass": "1.97.2",
|
||||
"shiki": "3.21.0",
|
||||
"sass": "1.97.1",
|
||||
"shiki": "3.20.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.182.0",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "7.3.1",
|
||||
"vite": "7.3.0",
|
||||
"vue": "3.5.26",
|
||||
"wanakana": "5.3.1"
|
||||
},
|
||||
@@ -81,7 +81,7 @@
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
"@storybook/addon-essentials": "8.6.15",
|
||||
"@storybook/addon-interactions": "8.6.15",
|
||||
"@storybook/addon-links": "10.1.11",
|
||||
"@storybook/addon-links": "10.1.10",
|
||||
"@storybook/addon-mdx-gfm": "8.6.15",
|
||||
"@storybook/addon-storysource": "8.6.15",
|
||||
"@storybook/blocks": "8.6.15",
|
||||
@@ -89,13 +89,13 @@
|
||||
"@storybook/core-events": "8.6.15",
|
||||
"@storybook/manager-api": "8.6.15",
|
||||
"@storybook/preview-api": "8.6.15",
|
||||
"@storybook/react": "10.1.11",
|
||||
"@storybook/react-vite": "10.1.11",
|
||||
"@storybook/react": "10.1.10",
|
||||
"@storybook/react-vite": "10.1.10",
|
||||
"@storybook/test": "8.6.15",
|
||||
"@storybook/theming": "8.6.15",
|
||||
"@storybook/types": "8.6.15",
|
||||
"@storybook/vue3": "10.1.11",
|
||||
"@storybook/vue3-vite": "10.1.11",
|
||||
"@storybook/vue3": "10.1.10",
|
||||
"@storybook/vue3-vite": "10.1.10",
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
@@ -103,46 +103,46 @@
|
||||
"@types/insert-text-at-cursor": "0.3.2",
|
||||
"@types/matter-js": "0.20.2",
|
||||
"@types/micromatch": "4.0.10",
|
||||
"@types/node": "24.10.9",
|
||||
"@types/node": "24.10.4",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@types/textarea-caret": "3.0.4",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@vitest/coverage-v8": "4.0.17",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"@vue/compiler-core": "3.5.26",
|
||||
"acorn": "8.15.0",
|
||||
"astring": "1.9.0",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.9.0",
|
||||
"cypress": "15.8.1",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.7.0",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"estree-walker": "3.0.3",
|
||||
"happy-dom": "20.3.1",
|
||||
"happy-dom": "20.0.11",
|
||||
"intersection-observer": "0.12.2",
|
||||
"magic-string": "0.30.21",
|
||||
"micromatch": "4.0.8",
|
||||
"minimatch": "10.1.1",
|
||||
"msw": "2.12.7",
|
||||
"msw": "2.12.6",
|
||||
"msw-storybook-addon": "2.0.6",
|
||||
"nodemon": "3.1.11",
|
||||
"prettier": "3.8.0",
|
||||
"prettier": "3.7.4",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "2.1.3",
|
||||
"storybook": "10.1.11",
|
||||
"storybook": "10.1.10",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"tsx": "4.21.0",
|
||||
"vite-plugin-glsl": "1.5.5",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "4.0.17",
|
||||
"vitest": "4.0.16",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
"vue-component-type-helpers": "3.2.2",
|
||||
"vue-component-type-helpers": "3.2.1",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.2.2"
|
||||
"vue-tsc": "3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div :class="$style.body">
|
||||
<div :class="$style.header">
|
||||
<span :class="$style.title">{{ i18n.ts._achievements._types[`_${achievement.name}`].title }}</span>
|
||||
<span :class="$style.title">{{ (i18n.ts._achievements._types as any)['_' + achievement.name].title }}</span>
|
||||
<span :class="$style.time">
|
||||
<time v-tooltip="new Date(achievement.unlockedAt).toLocaleString()">{{ new Date(achievement.unlockedAt).getFullYear() }}/{{ new Date(achievement.unlockedAt).getMonth() + 1 }}/{{ new Date(achievement.unlockedAt).getDate() }}</time>
|
||||
</span>
|
||||
</div>
|
||||
<div :class="$style.description">{{ withDescription ? i18n.ts._achievements._types[`_${achievement.name}`].description : '???' }}</div>
|
||||
<div v-if="'flavor' in i18n.ts._achievements._types[`_${achievement.name}`] && withDescription" :class="$style.flavor">{{ (i18n.ts._achievements._types[`_${achievement.name}`] as { flavor: string; }).flavor }}</div>
|
||||
<div :class="$style.description">{{ withDescription ? (i18n.ts._achievements._types as any)['_' + achievement.name].description : '???' }}</div>
|
||||
<div v-if="(i18n.ts._achievements._types as any)['_' + achievement.name].flavor && withDescription" :class="$style.flavor">{{ (i18n.ts._achievements._types as any)['_' + achievement.name].flavor }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="withLocked">
|
||||
|
||||
@@ -45,7 +45,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts">
|
||||
import { markRaw, ref, useTemplateRef, computed, onUpdated, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { emojilist, getEmojiName } from '@@/js/emojilist.js';
|
||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
|
||||
@@ -64,7 +63,7 @@ import { prefer } from '@/preferences.js';
|
||||
|
||||
export type CompleteInfo = {
|
||||
user: {
|
||||
payload: Misskey.entities.User;
|
||||
payload: any;
|
||||
query: string | null;
|
||||
},
|
||||
hashtag: {
|
||||
@@ -186,9 +185,9 @@ const suggests = ref<Element>();
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
|
||||
const fetching = ref(true);
|
||||
const users = ref<Misskey.entities.User[]>([]);
|
||||
const hashtags = ref<string[]>([]);
|
||||
const emojis = ref<EmojiDef[]>([]);
|
||||
const users = ref<any[]>([]);
|
||||
const hashtags = ref<any[]>([]);
|
||||
const emojis = ref<(EmojiDef)[]>([]);
|
||||
const items = ref<Element[] | HTMLCollection>([]);
|
||||
const mfmTags = ref<string[]>([]);
|
||||
const mfmParams = ref<string[]>([]);
|
||||
@@ -205,8 +204,8 @@ function complete<T extends keyof CompleteInfo>(type: T, value: CompleteInfo[T][
|
||||
emit('closed');
|
||||
if (type === 'emoji' || type === 'emojiComplete') {
|
||||
let recents = store.s.recentlyUsedEmojis;
|
||||
recents = recents.filter((emoji) => emoji !== value);
|
||||
recents.unshift(value as string);
|
||||
recents = recents.filter((emoji: any) => emoji !== value);
|
||||
recents.unshift(value);
|
||||
store.set('recentlyUsedEmojis', recents.splice(0, 32));
|
||||
}
|
||||
}
|
||||
@@ -255,7 +254,7 @@ function exec() {
|
||||
limit: 10,
|
||||
detail: false,
|
||||
}).then(searchedUsers => {
|
||||
users.value = searchedUsers;
|
||||
users.value = searchedUsers as any[];
|
||||
fetching.value = false;
|
||||
// キャッシュ
|
||||
sessionStorage.setItem(cacheKey, JSON.stringify(searchedUsers));
|
||||
@@ -277,7 +276,7 @@ function exec() {
|
||||
query: props.q,
|
||||
limit: 30,
|
||||
}).then(searchedHashtags => {
|
||||
hashtags.value = searchedHashtags;
|
||||
hashtags.value = searchedHashtags as any[];
|
||||
fetching.value = false;
|
||||
// キャッシュ
|
||||
sessionStorage.setItem(cacheKey, JSON.stringify(searchedHashtags));
|
||||
|
||||
@@ -7,12 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div>
|
||||
<span v-if="!available">Loading<MkEllipsis/></span>
|
||||
<div v-if="props.provider == 'mcaptcha'">
|
||||
<iframe
|
||||
v-if="mCaptchaIframeUrl != null"
|
||||
ref="mCaptchaIframe"
|
||||
:src="mCaptchaIframeUrl"
|
||||
style="border: none; max-width: 320px; width: 100%; height: 100%; max-height: 80px;"
|
||||
></iframe>
|
||||
<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
|
||||
<div ref="captchaEl"></div>
|
||||
</div>
|
||||
<div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;">
|
||||
<img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/>
|
||||
@@ -30,8 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, useTemplateRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted, nextTick } from 'vue';
|
||||
import type Reciever_typeReferenceOnly from '@mcaptcha/core-glue';
|
||||
import { ref, useTemplateRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
|
||||
import { store } from '@/store.js';
|
||||
|
||||
// APIs provided by Captcha services
|
||||
@@ -76,19 +71,6 @@ const available = ref(false);
|
||||
|
||||
const captchaEl = useTemplateRef('captchaEl');
|
||||
const captchaWidgetId = ref<string | undefined>(undefined);
|
||||
|
||||
let mCaptchaReciever: Reciever_typeReferenceOnly | null = null;
|
||||
const mCaptchaIframe = useTemplateRef('mCaptchaIframe');
|
||||
const mCaptchaRemoveState = ref(false);
|
||||
const mCaptchaIframeUrl = computed(() => {
|
||||
if (props.provider === 'mcaptcha' && !mCaptchaRemoveState.value && props.instanceUrl && props.sitekey) {
|
||||
const url = new URL('/widget', props.instanceUrl);
|
||||
url.searchParams.set('sitekey', props.sitekey);
|
||||
return url.toString();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const testcaptchaInput = ref('');
|
||||
const testcaptchaPassed = ref(false);
|
||||
|
||||
@@ -147,14 +129,8 @@ function reset() {
|
||||
if (_DEV_) console.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
testcaptchaPassed.value = false;
|
||||
testcaptchaInput.value = '';
|
||||
|
||||
if (mCaptchaReciever != null) {
|
||||
mCaptchaReciever.destroy();
|
||||
mCaptchaReciever = null;
|
||||
}
|
||||
}
|
||||
|
||||
function remove() {
|
||||
@@ -167,10 +143,6 @@ function remove() {
|
||||
if (_DEV_) console.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.provider === 'mcaptcha') {
|
||||
mCaptchaRemoveState.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestRender() {
|
||||
@@ -188,29 +160,32 @@ async function requestRender() {
|
||||
'error-callback': () => callback(undefined),
|
||||
});
|
||||
} else if (props.provider === 'mcaptcha' && props.instanceUrl && props.sitekey) {
|
||||
const { default: Reciever } = await import('@mcaptcha/core-glue');
|
||||
mCaptchaReciever = new Reciever({
|
||||
const { default: Widget } = await import('@mcaptcha/vanilla-glue');
|
||||
new Widget({
|
||||
siteKey: {
|
||||
key: props.sitekey,
|
||||
instanceUrl: new URL(props.instanceUrl),
|
||||
key: props.sitekey,
|
||||
},
|
||||
}, (token: string) => {
|
||||
callback(token);
|
||||
});
|
||||
mCaptchaReciever.listen();
|
||||
mCaptchaRemoveState.value = false;
|
||||
} else {
|
||||
window.setTimeout(requestRender, 50);
|
||||
window.setTimeout(requestRender, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function clearWidget() {
|
||||
reset();
|
||||
remove();
|
||||
if (props.provider === 'mcaptcha') {
|
||||
const container = window.document.getElementById('mcaptcha__widget-container');
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
} else {
|
||||
reset();
|
||||
remove();
|
||||
|
||||
if (captchaEl.value) {
|
||||
// レンダリング先のコンテナの中身を掃除し、フォームが増殖するのを抑止
|
||||
captchaEl.value.innerHTML = '';
|
||||
if (captchaEl.value) {
|
||||
// レンダリング先のコンテナの中身を掃除し、フォームが増殖するのを抑止
|
||||
captchaEl.value.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
|
||||
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"></span>
|
||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"></span>
|
||||
<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"/>
|
||||
<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkSelect v-if="select" v-model="selectedValue" :items="selectDef" autofocus></MkSelect>
|
||||
@@ -52,8 +52,7 @@ import MkModal from '@/components/MkModal.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||
import type { OptionValue } from '@/types/option-value.js';
|
||||
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
borderWidth ? { borderWidth: borderWidth } : {},
|
||||
borderColor ? { borderColor: borderColor } : {},
|
||||
]"
|
||||
></div>
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #key>{{ i18n.ts.permission }}</template>
|
||||
<template #value>
|
||||
<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
|
||||
<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] ?? permission }}</li>
|
||||
<li v-for="permission in extension.meta.permissions" :key="permission">{{ (i18n.ts._permissions as any)[permission] ?? permission }}</li>
|
||||
</ul>
|
||||
<template v-else>{{ i18n.ts.none }}</template>
|
||||
</template>
|
||||
@@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkFolder>
|
||||
</div>
|
||||
|
||||
<slot name="additionalInfo"></slot>
|
||||
<slot name="additionalInfo"/>
|
||||
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton danger rounded large @click="emits('cancel')"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
|
||||
@@ -101,8 +101,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
||||
export type Extension = {
|
||||
type: 'plugin';
|
||||
raw: string;
|
||||
@@ -111,7 +109,7 @@ export type Extension = {
|
||||
version: string;
|
||||
author: string;
|
||||
description?: string;
|
||||
permissions?: (typeof Misskey.permissions)[number][];
|
||||
permissions?: string[];
|
||||
config?: Record<string, unknown>;
|
||||
};
|
||||
} | {
|
||||
@@ -127,6 +125,7 @@ export type Extension = {
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import FormSplit from '@/components/form/split.vue';
|
||||
import MkCode from '@/components/MkCode.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
|
||||
@@ -26,8 +26,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSelect v-else-if="v.type === 'enum'" v-model="values[k]" :items="getMkSelectDef(v)">
|
||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||
</MkSelect>
|
||||
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]" :options="getRadioOptionsDef(v)">
|
||||
<MkRadios v-else-if="v.type === 'radio'" v-model="values[k]">
|
||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||
<option v-for="option in v.options" :key="getRadioKey(option)" :value="option.value">{{ option.label }}</option>
|
||||
</MkRadios>
|
||||
<MkRange v-else-if="v.type === 'range'" v-model="values[k]" :min="v.min" :max="v.max" :step="v.step" :textConverter="v.textConverter">
|
||||
<template #label><span v-text="v.label || k"></span><span v-if="v.required === false"> ({{ i18n.ts.optional }})</span></template>
|
||||
@@ -59,7 +60,6 @@ import MkButton from '@/components/MkButton.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||
import type { MkRadiosOption } from '@/components/MkRadios.vue';
|
||||
import type { Form, EnumFormItem, RadioFormItem } from '@/utility/form.js';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -113,13 +113,7 @@ function getMkSelectDef(def: EnumFormItem): MkSelectItem[] {
|
||||
});
|
||||
}
|
||||
|
||||
function getRadioOptionsDef(def: RadioFormItem): MkRadiosOption[] {
|
||||
return def.options.map<MkRadiosOption>((v) => {
|
||||
if (typeof v === 'string') {
|
||||
return { value: v, label: v };
|
||||
} else {
|
||||
return { value: v.value, label: v.label };
|
||||
}
|
||||
});
|
||||
function getRadioKey(e: RadioFormItem['options'][number]) {
|
||||
return typeof e.value === 'string' ? e.value : JSON.stringify(e.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -28,9 +28,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ v.label ?? k }}</template>
|
||||
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
|
||||
</MkRange>
|
||||
<MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]" :options="v.enum">
|
||||
<MkRadios v-else-if="v.type === 'number:enum'" v-model="params[k]">
|
||||
<template #label>{{ v.label ?? k }}</template>
|
||||
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
|
||||
<option v-for="item in v.enum" :value="item.value">
|
||||
<i v-if="item.icon" :class="item.icon"></i>
|
||||
<template v-else>{{ item.label }}</template>
|
||||
</option>
|
||||
</MkRadios>
|
||||
<div v-else-if="v.type === 'seed'">
|
||||
<MkRange v-model="params[k]" continuousUpdate type="number" :min="0" :max="10000" :step="1">
|
||||
|
||||
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
draggable="false"
|
||||
tabindex="-1"
|
||||
style="-webkit-user-drag: none;"
|
||||
></canvas>
|
||||
/>
|
||||
<img
|
||||
v-show="!hide"
|
||||
key="img"
|
||||
|
||||
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@input="onInput"
|
||||
>
|
||||
<datalist v-if="datalist" :id="id">
|
||||
<option v-for="data in datalist" :key="data" :value="data"></option>
|
||||
<option v-for="data in datalist" :key="data" :value="data"/>
|
||||
</datalist>
|
||||
<div ref="suffixEl" :class="$style.suffix"><slot name="suffix"></slot></div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="reveal" @contextmenu.stop="onContextmenu">
|
||||
<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="reveal">
|
||||
<component
|
||||
:is="disableImageLink ? 'div' : 'a'"
|
||||
v-bind="disableImageLink ? {
|
||||
@@ -123,7 +123,7 @@ watch(() => props.image, (newImage) => {
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
function getMenu() {
|
||||
function showMenu(ev: PointerEvent) {
|
||||
const menuItems: MenuItem[] = [];
|
||||
|
||||
menuItems.push({
|
||||
@@ -188,16 +188,9 @@ function getMenu() {
|
||||
});
|
||||
}
|
||||
|
||||
return menuItems;
|
||||
os.popupMenu(menuItems, ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function showMenu(ev: PointerEvent) {
|
||||
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
|
||||
}
|
||||
|
||||
function onContextmenu(ev: PointerEvent) {
|
||||
os.contextMenu(getMenu(), ev);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
@@ -323,20 +323,9 @@ async function showRadioOptions(item: MenuRadio, ev: MouseEvent | PointerEvent |
|
||||
type: 'radioOption',
|
||||
text: key,
|
||||
action: () => {
|
||||
if ('value' in item.ref) {
|
||||
item.ref.value = value;
|
||||
} else {
|
||||
// @ts-expect-error リアクティビティは保たれる
|
||||
item.ref = value;
|
||||
}
|
||||
item.ref = value;
|
||||
},
|
||||
active: computed(() => {
|
||||
if ('value' in item.ref) {
|
||||
return item.ref.value === value;
|
||||
} else {
|
||||
return item.ref === value;
|
||||
}
|
||||
}),
|
||||
active: computed(() => item.ref === value),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
|
||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||
<i v-else-if="notification.type === 'spReaction'" class="ti ti-octahedron-plus"></i>
|
||||
<i v-else-if="notification.type === 'scheduledNotePosted'" class="ti ti-send"></i>
|
||||
<i v-else-if="notification.type === 'scheduledNotePostFailed'" class="ti ti-alert-triangle"></i>
|
||||
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
|
||||
@@ -75,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<span v-else-if="notification.type === 'createToken'">{{ i18n.ts._notification.createToken }}</span>
|
||||
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
|
||||
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
|
||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'spReaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
|
||||
<span v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'">{{ i18n.tsx._notification.likedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||
<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: getActualReactedUsersCount(notification) }) }}</span>
|
||||
<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
|
||||
@@ -83,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
|
||||
</header>
|
||||
<div>
|
||||
<MkA v-if="notification.type === 'reaction' || notification.type === 'reaction:grouped' || notification.type === 'spReaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<MkA v-if="notification.type === 'reaction' || notification.type === 'reaction:grouped'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
|
||||
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||
@@ -122,7 +121,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
{{ notification.invitation.room.name }}
|
||||
</div>
|
||||
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
|
||||
{{ i18n.ts._achievements._types[`_${notification.achievement}`].title }}
|
||||
{{ (i18n.ts._achievements._types as any)['_' + notification.achievement].title }}
|
||||
</MkA>
|
||||
<MkA v-else-if="notification.type === 'exportCompleted'" :class="$style.text" :to="`/my/drive/file/${notification.fileId}`">
|
||||
{{ i18n.ts.showFile }}
|
||||
@@ -144,8 +143,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template v-else-if="notification.type === 'receiveFollowRequest'">
|
||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
|
||||
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
|
||||
<MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ti ti-check"></i> {{ i18n.ts.accept }}</MkButton>
|
||||
<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ti ti-x"></i> {{ i18n.ts.reject }}</MkButton>
|
||||
<MkButton :class="$style.followRequestCommandButton" rounded primary @click="acceptFollowRequest()"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
|
||||
<MkButton :class="$style.followRequestCommandButton" rounded danger @click="rejectFollowRequest()"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else-if="notification.type === 'test'" :class="$style.text">{{ i18n.ts._notification.notificationWillBeDisplayedLikeThis }}</span>
|
||||
|
||||
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.buttons">
|
||||
<div v-if="prevDotVisible" :class="$style.headTailButtons">
|
||||
<MkButton @click="onToHeadButtonClicked">{{ min }}</MkButton>
|
||||
<span class="ti ti-dots"></span>
|
||||
<span class="ti ti-dots"/>
|
||||
</div>
|
||||
|
||||
<MkButton
|
||||
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkButton>
|
||||
|
||||
<div v-if="nextDotVisible" :class="$style.headTailButtons">
|
||||
<span class="ti ti-dots"></span>
|
||||
<span class="ti ti-dots"/>
|
||||
<MkButton @click="onToTailButtonClicked">{{ max }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"></div>
|
||||
<div :class="[$style.root, accented ? $style.accented : null, revered ? $style.revered : null]"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
@@ -77,7 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
|
||||
<div v-if="targetChannel" :class="$style.colorBar" :style="{ background: targetChannel.color }"></div>
|
||||
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"></textarea>
|
||||
<textarea ref="textareaEl" v-model="text" :class="[$style.text]" :disabled="posting || posted" :readonly="textAreaReadOnly" :placeholder="placeholder" data-cy-post-form-text @keydown="onKeydown" @keyup="onKeyup" @paste="onPaste" @compositionupdate="onCompositionUpdate" @compositionend="onCompositionEnd"/>
|
||||
<div v-if="maxTextLength - textLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: textLength > maxTextLength }]">{{ maxTextLength - textLength }}</div>
|
||||
</div>
|
||||
<input v-show="withHashtags" ref="hashtagsInputEl" v-model="hashtags" :class="$style.hashtags" :placeholder="i18n.ts.hashtags" list="hashtags">
|
||||
@@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</footer>
|
||||
<datalist id="hashtags">
|
||||
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"></option>
|
||||
<option v-for="hashtag in recentHashtags" :key="hashtag" :value="hashtag"/>
|
||||
</datalist>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
tabindex="0"
|
||||
@click="showFileMenu(item, $event)"
|
||||
@keydown.space.enter="showFileMenu(item, $event)"
|
||||
@contextmenu.prevent.stop="showFileMenu(item, $event)"
|
||||
@contextmenu.prevent="showFileMenu(item, $event)"
|
||||
>
|
||||
<!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる -->
|
||||
<MkDriveFileThumbnail style="pointer-events: none;" :data-id="item.id" :class="$style.thumbnail" :file="item" fit="cover"/>
|
||||
|
||||
@@ -12,6 +12,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSwitch v-model="flag" :class="$style.preview__content1__switch_button">
|
||||
<span>Switch is now {{ flag ? 'on' : 'off' }}</span>
|
||||
</MkSwitch>
|
||||
<div :class="$style.preview__content1__input">
|
||||
<MkRadio v-model="radio" value="misskey">Misskey</MkRadio>
|
||||
<MkRadio v-model="radio" value="mastodon">Mastodon</MkRadio>
|
||||
<MkRadio v-model="radio" value="pleroma">Pleroma</MkRadio>
|
||||
</div>
|
||||
<div :class="$style.preview__content1__button">
|
||||
<MkButton inline>This is</MkButton>
|
||||
<MkButton inline primary>the button</MkButton>
|
||||
@@ -35,12 +40,15 @@ import * as config from '@@/js/config.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkRadio from '@/components/MkRadio.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { chooseDriveFile } from '@/utility/drive.js';
|
||||
|
||||
const text = ref('');
|
||||
const flag = ref(true);
|
||||
const radio = ref('misskey');
|
||||
const mfm = ref(`Hello world! This is an @example mention. BTW you are @${$i ? $i.username : 'guest'}.\nAlso, here is ${config.url} and [example link](${config.url}). for more details, see https://example.com.\nAs you know #misskey is open-source software.`);
|
||||
|
||||
const openDialog = async () => {
|
||||
|
||||
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
<slot/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ async function unsubscribe() {
|
||||
}
|
||||
|
||||
function encode(buffer: ArrayBuffer | null) {
|
||||
return btoa(String.fromCharCode(...(buffer != null ? new Uint8Array(buffer) : [])));
|
||||
return btoa(String.fromCharCode.apply(null, buffer ? new Uint8Array(buffer) as any : []));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-adaptive-border
|
||||
:class="[$style.root, { [$style.disabled]: disabled, [$style.checked]: checked }]"
|
||||
:aria-checked="checked"
|
||||
:aria-disabled="disabled"
|
||||
role="checkbox"
|
||||
@click="toggle"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
:disabled="disabled"
|
||||
:class="$style.input"
|
||||
>
|
||||
<span :class="$style.button">
|
||||
<span></span>
|
||||
</span>
|
||||
<span :class="$style.label"><slot></slot></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup generic="T extends unknown">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: T;
|
||||
value: T;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: T): void;
|
||||
}>();
|
||||
|
||||
const checked = computed(() => props.modelValue === props.value);
|
||||
|
||||
function toggle(): void {
|
||||
if (props.disabled) return;
|
||||
emit('update:modelValue', props.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
padding: 7px 10px;
|
||||
min-width: 60px;
|
||||
background-color: var(--MI_THEME-panel);
|
||||
background-clip: padding-box !important;
|
||||
border: solid 1px var(--MI_THEME-panel);
|
||||
border-radius: 6px;
|
||||
font-size: 90%;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--MI_THEME-inputBorderHover) !important;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--MI_THEME-focus);
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background-color: var(--MI_THEME-accentedBg) !important;
|
||||
border-color: var(--MI_THEME-accentedBg) !important;
|
||||
color: var(--MI_THEME-accent);
|
||||
cursor: default !important;
|
||||
|
||||
> .button {
|
||||
border-color: var(--MI_THEME-accent);
|
||||
|
||||
&::after {
|
||||
background-color: var(--MI_THEME-accent);
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: none;
|
||||
border: solid 2px var(--MI_THEME-inputBorder);
|
||||
border-radius: 100%;
|
||||
transition: inherit;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
border-radius: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-left: 8px;
|
||||
display: block;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@@ -3,225 +3,99 @@ SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="{ [$style.vertical]: vertical }">
|
||||
<div :class="$style.label">
|
||||
<slot name="label"></slot>
|
||||
</div>
|
||||
|
||||
<div :class="$style.body">
|
||||
<div
|
||||
v-for="option in options"
|
||||
:key="getKey(option.value)"
|
||||
v-adaptive-border
|
||||
:class="[$style.optionRoot, { [$style.disabled]: option.disabled, [$style.checked]: model === option.value }]"
|
||||
:aria-checked="model === option.value"
|
||||
:aria-disabled="option.disabled"
|
||||
role="checkbox"
|
||||
@click="toggle(option)"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
:disabled="option.disabled"
|
||||
:class="$style.optionInput"
|
||||
>
|
||||
<span :class="$style.optionButton">
|
||||
<span></span>
|
||||
</span>
|
||||
<div :class="$style.optionContent">
|
||||
<i v-if="option.icon" :class="[$style.optionIcon, option.icon]" :style="option.iconStyle"></i>
|
||||
<div>
|
||||
<slot v-if="option.slotId != null" :name="`option-${option.slotId as SlotNames}`"></slot>
|
||||
<template v-else>
|
||||
<div :style="option.labelStyle">{{ option.label ?? option.value }}</div>
|
||||
<div v-if="option.caption" :class="$style.optionCaption">{{ option.caption }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.caption">
|
||||
<slot name="caption"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { StyleValue } from 'vue';
|
||||
import type { OptionValue } from '@/types/option-value.js';
|
||||
import { Comment, defineComponent, h, ref, watch } from 'vue';
|
||||
import MkRadio from './MkRadio.vue';
|
||||
import type { VNode } from 'vue';
|
||||
|
||||
export type MkRadiosOption<T = OptionValue, S = string> = {
|
||||
value: T;
|
||||
slotId?: S;
|
||||
label?: string;
|
||||
labelStyle?: StyleValue;
|
||||
icon?: string;
|
||||
iconStyle?: StyleValue;
|
||||
caption?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
export default defineComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
required: false,
|
||||
},
|
||||
vertical: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const value = ref(props.modelValue);
|
||||
watch(value, () => {
|
||||
context.emit('update:modelValue', value.value);
|
||||
});
|
||||
watch(() => props.modelValue, v => {
|
||||
value.value = v;
|
||||
});
|
||||
if (!context.slots.default) return null;
|
||||
let options = context.slots.default();
|
||||
const label = context.slots.label && context.slots.label();
|
||||
const caption = context.slots.caption && context.slots.caption();
|
||||
|
||||
// なぜかFragmentになることがあるため
|
||||
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
|
||||
|
||||
// vnodeのうちv-if=falseなものを除外する(trueになるものはoptionなど他typeになる)
|
||||
options = options.filter(vnode => vnode.type !== Comment);
|
||||
|
||||
return () => h('div', {
|
||||
class: [
|
||||
'novjtcto',
|
||||
...(props.vertical ? ['vertical'] : []),
|
||||
],
|
||||
}, [
|
||||
...(label ? [h('div', {
|
||||
class: 'label',
|
||||
}, label)] : []),
|
||||
h('div', {
|
||||
class: 'body',
|
||||
}, options.map(option => h(MkRadio, {
|
||||
key: option.key as string,
|
||||
value: option.props?.value,
|
||||
disabled: option.props?.disabled,
|
||||
modelValue: value.value,
|
||||
'onUpdate:modelValue': _v => value.value = _v,
|
||||
}, () => option.children)),
|
||||
),
|
||||
...(caption ? [h('div', {
|
||||
class: 'caption',
|
||||
}, caption)] : []),
|
||||
]);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<script setup lang="ts" generic="const T extends MkRadiosOption">
|
||||
defineProps<{
|
||||
options: T[];
|
||||
vertical?: boolean;
|
||||
}>();
|
||||
<style lang="scss">
|
||||
.novjtcto {
|
||||
> .label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 0;
|
||||
user-select: none;
|
||||
|
||||
type SlotNames = NonNullable<T extends MkRadiosOption<any, infer U> ? U : never>;
|
||||
|
||||
defineSlots<{
|
||||
label?: () => void;
|
||||
caption?: () => void;
|
||||
} & {
|
||||
[K in `option-${SlotNames}`]: () => void;
|
||||
}>();
|
||||
|
||||
const model = defineModel<T['value']>({ required: true });
|
||||
|
||||
function getKey(value: OptionValue): PropertyKey {
|
||||
if (value === null) return '___null___';
|
||||
return value;
|
||||
}
|
||||
|
||||
function toggle(o: MkRadiosOption): void {
|
||||
if (o.disabled) return;
|
||||
model.value = o.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.label {
|
||||
font-size: 0.85em;
|
||||
padding: 0 0 8px 0;
|
||||
user-select: none;
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 0.85em;
|
||||
padding: 8px 0 0 0;
|
||||
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical > .body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.optionRoot {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
padding: 8px 10px;
|
||||
min-width: 60px;
|
||||
background-color: var(--MI_THEME-panel);
|
||||
background-clip: padding-box !important;
|
||||
border: solid 1px var(--MI_THEME-panel);
|
||||
border-radius: 6px;
|
||||
font-size: 90%;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--MI_THEME-inputBorderHover) !important;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--MI_THEME-focus);
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background-color: var(--MI_THEME-accentedBg) !important;
|
||||
border-color: var(--MI_THEME-accentedBg) !important;
|
||||
color: var(--MI_THEME-accent);
|
||||
cursor: default !important;
|
||||
|
||||
.optionButton {
|
||||
border-color: var(--MI_THEME-accent);
|
||||
|
||||
&::after {
|
||||
background-color: var(--MI_THEME-accent);
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.optionCaption {
|
||||
color: color(from var(--MI_THEME-accent) srgb r g b / 0.75);
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.optionInput {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.optionButton {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: none;
|
||||
border: solid 2px var(--MI_THEME-inputBorder);
|
||||
border-radius: 100%;
|
||||
transition: inherit;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
border-radius: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition: 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
> .body {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.optionContent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
> .caption {
|
||||
font-size: 0.85em;
|
||||
padding: 8px 0 0 0;
|
||||
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
|
||||
|
||||
.optionCaption {
|
||||
font-size: 0.85em;
|
||||
padding: 2px 0 0 0;
|
||||
color: color(from var(--MI_THEME-fg) srgb r g b / 0.75);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.optionIcon {
|
||||
flex-shrink: 0;
|
||||
&.vertical {
|
||||
> .body {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:myReaction="props.myReaction"
|
||||
@reactionToggled="onMockToggleReaction"
|
||||
/>
|
||||
<slot v-if="hasMoreReactions" name="more"></slot>
|
||||
<slot v-if="hasMoreReactions" name="more"/>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, useTemplateRef } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import type { ScatterDataPoint } from 'chart.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { store } from '@/store.js';
|
||||
import { useChartTooltip } from '@/composables/use-chart-tooltip.js';
|
||||
@@ -19,12 +18,6 @@ import { alpha } from '@/utility/color.js';
|
||||
import { initChart } from '@/utility/init-chart.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
|
||||
interface RetentionPoint extends ScatterDataPoint {
|
||||
x: number;
|
||||
y: number;
|
||||
d: string;
|
||||
}
|
||||
|
||||
initChart();
|
||||
|
||||
const chartEl = useTemplateRef('chartEl');
|
||||
@@ -69,14 +62,14 @@ onMounted(async () => {
|
||||
fill: false,
|
||||
tension: 0.4,
|
||||
data: [{
|
||||
x: 0,
|
||||
x: '0',
|
||||
y: 100,
|
||||
d: getYYYYMMDD(new Date(record.createdAt)),
|
||||
}, ...Object.entries(record.data).sort((a, b) => getDate(a[0]) > getDate(b[0]) ? 1 : -1).map(([k, v], i) => ({
|
||||
x: i + 1,
|
||||
x: (i + 1).toString(),
|
||||
y: (v / record.users) * 100,
|
||||
d: getYYYYMMDD(new Date(record.createdAt)),
|
||||
}))],
|
||||
}))] as any,
|
||||
})),
|
||||
},
|
||||
options: {
|
||||
@@ -118,11 +111,11 @@ onMounted(async () => {
|
||||
enabled: false,
|
||||
callbacks: {
|
||||
title(context) {
|
||||
const v = context[0].dataset.data[context[0].dataIndex] as RetentionPoint;
|
||||
const v = context[0].dataset.data[context[0].dataIndex] as unknown as { x: string, y: number, d: string };
|
||||
return `${v.x} days later`;
|
||||
},
|
||||
label(context) {
|
||||
const v = context.dataset.data[context.dataIndex] as RetentionPoint;
|
||||
const v = context.dataset.data[context.dataIndex] as unknown as { x: string, y: number, d: string };
|
||||
const p = Math.round(v.y) + '%';
|
||||
return `${v.d} ${p}`;
|
||||
},
|
||||
|
||||
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { OptionValue } from '@/types/option-value.js';
|
||||
export type OptionValue = string | number | null;
|
||||
|
||||
export type ItemOption<T extends OptionValue = OptionValue> = {
|
||||
type?: 'option';
|
||||
|
||||
@@ -14,15 +14,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #icon><i class="ti ti-settings-question"></i></template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkRadios
|
||||
v-model="q_use"
|
||||
:options="[
|
||||
{ value: 'single', label: i18n.ts._serverSetupWizard._use.single, icon: 'ti ti-user', caption: i18n.ts._serverSetupWizard._use.single_description },
|
||||
{ value: 'group', label: i18n.ts._serverSetupWizard._use.group, icon: 'ti ti-lock', caption: i18n.ts._serverSetupWizard._use.group_description },
|
||||
{ value: 'open', label: i18n.ts._serverSetupWizard._use.open, icon: 'ti ti-world', caption: i18n.ts._serverSetupWizard._use.open_description },
|
||||
]"
|
||||
vertical
|
||||
>
|
||||
<MkRadios v-model="q_use" :vertical="true">
|
||||
<option value="single">
|
||||
<div><i class="ti ti-user"></i> <b>{{ i18n.ts._serverSetupWizard._use.single }}</b></div>
|
||||
<div>{{ i18n.ts._serverSetupWizard._use.single_description }}</div>
|
||||
</option>
|
||||
<option value="group">
|
||||
<div><i class="ti ti-lock"></i> <b>{{ i18n.ts._serverSetupWizard._use.group }}</b></div>
|
||||
<div>{{ i18n.ts._serverSetupWizard._use.group_description }}</div>
|
||||
</option>
|
||||
<option value="open">
|
||||
<div><i class="ti ti-world"></i> <b>{{ i18n.ts._serverSetupWizard._use.open }}</b></div>
|
||||
<div>{{ i18n.ts._serverSetupWizard._use.open_description }}</div>
|
||||
</option>
|
||||
</MkRadios>
|
||||
|
||||
<MkInfo v-if="q_use === 'single'">{{ i18n.ts._serverSetupWizard._use.single_youCanCreateMultipleAccounts }}</MkInfo>
|
||||
@@ -36,15 +40,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #icon><i class="ti ti-users"></i></template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkRadios
|
||||
v-model="q_scale"
|
||||
:options="[
|
||||
{ value: 'small', label: i18n.ts._serverSetupWizard._scale.small, icon: 'ti ti-user' },
|
||||
{ value: 'medium', label: i18n.ts._serverSetupWizard._scale.medium, icon: 'ti ti-users' },
|
||||
{ value: 'large', label: i18n.ts._serverSetupWizard._scale.large, icon: 'ti ti-users-group' },
|
||||
]"
|
||||
vertical
|
||||
>
|
||||
<MkRadios v-model="q_scale" :vertical="true">
|
||||
<option value="small"><i class="ti ti-user"></i> {{ i18n.ts._serverSetupWizard._scale.small }}</option>
|
||||
<option value="medium"><i class="ti ti-users"></i> {{ i18n.ts._serverSetupWizard._scale.medium }}</option>
|
||||
<option value="large"><i class="ti ti-users-group"></i> {{ i18n.ts._serverSetupWizard._scale.large }}</option>
|
||||
</MkRadios>
|
||||
|
||||
<MkInfo v-if="q_scale === 'large'"><b>{{ i18n.ts.advice }}:</b> {{ i18n.ts._serverSetupWizard.largeScaleServerAdvice }}</MkInfo>
|
||||
@@ -58,14 +57,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps_s">
|
||||
<div>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description1 }}<br>{{ i18n.ts._serverSetupWizard.doYouConnectToFediverse_description2 }}<br><MkLink target="_blank" url="https://wikipedia.org/wiki/Fediverse">{{ i18n.ts.learnMore }}</MkLink></div>
|
||||
|
||||
<MkRadios
|
||||
v-model="q_federation"
|
||||
:options="[
|
||||
{ value: 'yes', label: i18n.ts.yes },
|
||||
{ value: 'no', label: i18n.ts.no },
|
||||
]"
|
||||
vertical
|
||||
>
|
||||
<MkRadios v-model="q_federation" :vertical="true">
|
||||
<option value="yes">{{ i18n.ts.yes }}</option>
|
||||
<option value="no">{{ i18n.ts.no }}</option>
|
||||
</MkRadios>
|
||||
|
||||
<MkInfo v-if="q_federation === 'yes'">{{ i18n.ts._serverSetupWizard.youCanConfigureMoreFederationSettingsLater }}</MkInfo>
|
||||
@@ -218,9 +212,9 @@ const props = withDefaults(defineProps<{
|
||||
});
|
||||
|
||||
const q_name = ref('');
|
||||
const q_use = ref<'single' | 'group' | 'open'>('single');
|
||||
const q_scale = ref<'small' | 'medium' | 'large'>('small');
|
||||
const q_federation = ref<'yes' | 'no'>('no');
|
||||
const q_use = ref('single');
|
||||
const q_scale = ref('small');
|
||||
const q_federation = ref('yes');
|
||||
const q_remoteContentsCleaning = ref(true);
|
||||
const q_adminName = ref('');
|
||||
const q_adminEmail = ref('');
|
||||
@@ -245,7 +239,7 @@ const serverSettings = computed<Misskey.entities.AdminUpdateMetaRequest>(() => {
|
||||
enableReactionsBuffering,
|
||||
clientOptions: {
|
||||
entrancePageStyle: q_use.value === 'open' ? 'classic' : 'simple',
|
||||
},
|
||||
} as any,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -22,26 +22,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkTextarea v-model="text">
|
||||
<template #label>{{ i18n.ts.text }}</template>
|
||||
</MkTextarea>
|
||||
<MkRadios
|
||||
v-model="icon"
|
||||
:options="[
|
||||
{ value: 'info', icon: 'ti ti-info-circle' },
|
||||
{ value: 'warning', icon: 'ti ti-alert-triangle', iconStyle: 'color: var(--MI_THEME-warn);' },
|
||||
{ value: 'error', icon: 'ti ti-circle-x', iconStyle: 'color: var(--MI_THEME-error);' },
|
||||
{ value: 'success', icon: 'ti ti-check', iconStyle: 'color: var(--MI_THEME-success);' },
|
||||
]"
|
||||
>
|
||||
<MkRadios v-model="icon">
|
||||
<template #label>{{ i18n.ts.icon }}</template>
|
||||
<option value="info"><i class="ti ti-info-circle"></i></option>
|
||||
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
|
||||
<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
|
||||
<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
|
||||
</MkRadios>
|
||||
<MkRadios
|
||||
v-model="display"
|
||||
:options="[
|
||||
{ value: 'normal', label: i18n.ts.normal },
|
||||
{ value: 'banner', label: i18n.ts.banner },
|
||||
{ value: 'dialog', label: i18n.ts.dialog },
|
||||
]"
|
||||
>
|
||||
<MkRadios v-model="display">
|
||||
<template #label>{{ i18n.ts.display }}</template>
|
||||
<option value="normal">{{ i18n.ts.normal }}</option>
|
||||
<option value="banner">{{ i18n.ts.banner }}</option>
|
||||
<option value="dialog">{{ i18n.ts.dialog }}</option>
|
||||
</MkRadios>
|
||||
<MkSwitch v-model="needConfirmationToRead">
|
||||
{{ i18n.ts._announcement.needConfirmationToRead }}
|
||||
|
||||
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@ok="save()"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header><i class="ti ti-icons"></i> {{ i18n.ts._widgets[widgetName] ?? widgetName }}</template>
|
||||
<template #header><i class="ti ti-icons"></i> {{ (i18n.ts._widgets as any)[widgetName] ?? widgetName }}</template>
|
||||
|
||||
<MkPreviewWithControls>
|
||||
<template #preview>
|
||||
@@ -50,14 +50,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { useTemplateRef, ref, computed, onBeforeUnmount, onMounted } from 'vue';
|
||||
import MkPreviewWithControls from './MkPreviewWithControls.vue';
|
||||
import type { Form } from '@/utility/form.js';
|
||||
import type { WidgetName } from '@/widgets/index.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkForm from '@/components/MkForm.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
widgetName: WidgetName;
|
||||
widgetName: string;
|
||||
form: Form;
|
||||
currentSettings: Record<string, any>;
|
||||
}>();
|
||||
|
||||
@@ -11,16 +11,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div v-if="user.isCat" :class="[$style.ears]">
|
||||
<div :class="$style.earLeft">
|
||||
<div v-if="false" :class="$style.layer">
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.earRight">
|
||||
<div v-if="false" :class="$style.layer">
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"></div>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
<div :class="$style.plot" :style="{ backgroundImage: `url(${JSON.stringify(url)})` }"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template>
|
||||
<span :class="$style.container">
|
||||
<span ref="content" :class="$style.content" :style="{ maxWidth: `${100 / minScale}%` }">
|
||||
<slot></slot>
|
||||
<slot/>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
import { ref } from 'vue';
|
||||
import type { Ref, MaybeRefOrGetter } from 'vue';
|
||||
import type { MkSelectItem, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
|
||||
import type { OptionValue } from '@/types/option-value.js';
|
||||
import type { MkSelectItem, OptionValue, GetMkSelectValueTypesFromDef } from '@/components/MkSelect.vue';
|
||||
|
||||
type UnwrapReadonlyItems<T> = T extends readonly (infer U)[] ? U[] : T;
|
||||
|
||||
|
||||
@@ -51,3 +51,9 @@ export async function fetchInstance(force = false): Promise<Misskey.entities.Met
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
export type ClientOptions = {
|
||||
entrancePageStyle: 'classic' | 'simple';
|
||||
showTimelineForVisitor: boolean;
|
||||
showActivitiesForVisitor: boolean;
|
||||
};
|
||||
|
||||
@@ -14,8 +14,7 @@ import type { Form, GetFormResultType } from '@/utility/form.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import type { PostFormProps } from '@/types/post-form.js';
|
||||
import type { UploaderFeatures } from '@/composables/use-uploader.js';
|
||||
import type { MkSelectItem } from '@/components/MkSelect.vue';
|
||||
import type { OptionValue } from '@/types/option-value.js';
|
||||
import type { MkSelectItem, OptionValue } from '@/components/MkSelect.vue';
|
||||
import type { MkDialogReturnType } from '@/components/MkDialog.vue';
|
||||
import type { OverloadToUnion } from '@/types/overload-to-union.js';
|
||||
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
|
||||
|
||||
@@ -37,8 +37,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts._abuseReport._notificationRecipient.notifiedWebhook }}</template>
|
||||
</MkSelect>
|
||||
<MkButton rounded :class="$style.systemWebhookEditButton" @click="onEditSystemWebhookClicked">
|
||||
<span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"></span>
|
||||
<span v-else class="ti ti-settings" style="line-height: normal"></span>
|
||||
<span v-if="systemWebhookId === null" class="ti ti-plus" style="line-height: normal"/>
|
||||
<span v-else class="ti ti-settings" style="line-height: normal"/>
|
||||
</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<div :class="$style.root" class="_panel _gaps_s">
|
||||
<div :class="$style.rightDivider" style="width: 80px;"><span :class="`ti ${methodIcon}`"></span> {{ methodName }}</div>
|
||||
<div :class="$style.rightDivider" style="width: 80px;"><span :class="`ti ${methodIcon}`"/> {{ methodName }}</div>
|
||||
<div :class="$style.rightDivider" style="flex: 0.5">{{ entity.name }}</div>
|
||||
<div :class="$style.rightDivider" style="flex: 1">
|
||||
<div v-if="method === 'email' && user">
|
||||
@@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<div :class="$style.recipientButtons" style="margin-left: auto">
|
||||
<button :class="$style.recipientButton" @click="onEditButtonClicked()">
|
||||
<span class="ti ti-settings"></span>
|
||||
<span class="ti ti-settings"/>
|
||||
</button>
|
||||
<button :class="$style.recipientButton" @click="onDeleteButtonClicked()">
|
||||
<span class="ti ti-trash"></span>
|
||||
<span class="ti ti-trash"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.root" class="_gaps_m">
|
||||
<div :class="$style.addButton">
|
||||
<MkButton primary @click="onAddButtonClicked">
|
||||
<span class="ti ti-plus"></span> {{ i18n.ts._abuseReport._notificationRecipient.createRecipient }}
|
||||
<span class="ti ti-plus"/> {{ i18n.ts._abuseReport._notificationRecipient.createRecipient }}
|
||||
</MkButton>
|
||||
</div>
|
||||
<div :class="$style.subMenus" class="_gaps_s">
|
||||
|
||||
@@ -22,17 +22,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<MkRadios
|
||||
v-model="ad.place"
|
||||
:options="[
|
||||
{ value: 'square' },
|
||||
{ value: 'horizontal' },
|
||||
{ value: 'horizontal-big' },
|
||||
]"
|
||||
>
|
||||
<MkRadios v-model="ad.place">
|
||||
<template #label>Form</template>
|
||||
<option value="square">square</option>
|
||||
<option value="horizontal">horizontal</option>
|
||||
<option value="horizontal-big">horizontal-big</option>
|
||||
</MkRadios>
|
||||
|
||||
<!--
|
||||
<div style="margin: 32px 0;">
|
||||
{{ i18n.ts.priority }}
|
||||
<MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio>
|
||||
<MkRadio v-model="ad.priority" value="middle">{{ i18n.ts.middle }}</MkRadio>
|
||||
<MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<FormSplit>
|
||||
<MkInput v-model="ad.ratio" type="number">
|
||||
<template #label>{{ i18n.ts.ratio }}</template>
|
||||
@@ -104,11 +109,7 @@ import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
type Ad = Misskey.entities.Ad & {
|
||||
place: 'square' | 'horizontal' | 'horizontal-big';
|
||||
};
|
||||
|
||||
const ads = ref<Ad[]>([]);
|
||||
const ads = ref<Misskey.entities.Ad[]>([]);
|
||||
|
||||
// ISO形式はTZがUTCになってしまうので、TZ分ずらして時間を初期化
|
||||
const localTime = new Date();
|
||||
@@ -135,7 +136,7 @@ misskeyApi('admin/ad/list', { publishing: publishing }).then(adsResponse => {
|
||||
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
||||
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
||||
return {
|
||||
...(r as Ad),
|
||||
...r,
|
||||
expiresAt: exdate.toISOString().slice(0, 16),
|
||||
startsAt: stdate.toISOString().slice(0, 16),
|
||||
};
|
||||
@@ -238,7 +239,7 @@ function more() {
|
||||
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
||||
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
||||
return {
|
||||
...(r as Ad),
|
||||
...r,
|
||||
expiresAt: exdate.toISOString().slice(0, 16),
|
||||
startsAt: stdate.toISOString().slice(0, 16),
|
||||
};
|
||||
@@ -255,7 +256,7 @@ function refresh() {
|
||||
exdate.setMilliseconds(exdate.getMilliseconds() - localTimeDiff);
|
||||
stdate.setMilliseconds(stdate.getMilliseconds() - localTimeDiff);
|
||||
return {
|
||||
...(r as Ad),
|
||||
...r,
|
||||
expiresAt: exdate.toISOString().slice(0, 16),
|
||||
startsAt: stdate.toISOString().slice(0, 16),
|
||||
};
|
||||
|
||||
@@ -45,26 +45,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkInput v-model="announcement.imageUrl" type="url">
|
||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||
</MkInput>
|
||||
<MkRadios
|
||||
v-model="announcement.icon"
|
||||
:options="[
|
||||
{ value: 'info', icon: 'ti ti-info-circle' },
|
||||
{ value: 'warning', icon: 'ti ti-alert-triangle', iconStyle: 'color: var(--MI_THEME-warn);' },
|
||||
{ value: 'error', icon: 'ti ti-circle-x', iconStyle: 'color: var(--MI_THEME-error);' },
|
||||
{ value: 'success', icon: 'ti ti-check', iconStyle: 'color: var(--MI_THEME-success);' },
|
||||
]"
|
||||
>
|
||||
<MkRadios v-model="announcement.icon">
|
||||
<template #label>{{ i18n.ts.icon }}</template>
|
||||
<option value="info"><i class="ti ti-info-circle"></i></option>
|
||||
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
|
||||
<option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
|
||||
<option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
|
||||
</MkRadios>
|
||||
<MkRadios
|
||||
v-model="announcement.display"
|
||||
:options="[
|
||||
{ value: 'normal', label: i18n.ts.normal },
|
||||
{ value: 'banner', label: i18n.ts.banner },
|
||||
{ value: 'dialog', label: i18n.ts.dialog },
|
||||
]"
|
||||
>
|
||||
<MkRadios v-model="announcement.display">
|
||||
<template #label>{{ i18n.ts.display }}</template>
|
||||
<option value="normal">{{ i18n.ts.normal }}</option>
|
||||
<option value="banner">{{ i18n.ts.banner }}</option>
|
||||
<option value="dialog">{{ i18n.ts.dialog }}</option>
|
||||
</MkRadios>
|
||||
<MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo>
|
||||
<MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription">
|
||||
|
||||
@@ -19,17 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<div class="_gaps_m">
|
||||
<MkRadios
|
||||
v-model="botProtectionForm.state.provider"
|
||||
:options="[
|
||||
{ value: 'none', label: `${i18n.ts.none} (${i18n.ts.notRecommended})` },
|
||||
{ value: 'hcaptcha', label: 'hCaptcha' },
|
||||
{ value: 'mcaptcha', label: 'mCaptcha' },
|
||||
{ value: 'recaptcha', label: 'reCAPTCHA' },
|
||||
{ value: 'turnstile', label: 'Turnstile' },
|
||||
{ value: 'testcaptcha', label: 'testCaptcha' },
|
||||
]"
|
||||
>
|
||||
<MkRadios v-model="botProtectionForm.state.provider">
|
||||
<option value="none">{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</option>
|
||||
<option value="hcaptcha">hCaptcha</option>
|
||||
<option value="mcaptcha">mCaptcha</option>
|
||||
<option value="recaptcha">reCAPTCHA</option>
|
||||
<option value="turnstile">Turnstile</option>
|
||||
<option value="testcaptcha">testCaptcha</option>
|
||||
</MkRadios>
|
||||
|
||||
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
|
||||
|
||||
@@ -9,14 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<SearchMarker path="/admin/branding" :label="i18n.ts.branding" :keywords="['branding']" icon="ti ti-paint">
|
||||
<div class="_gaps_m">
|
||||
<SearchMarker :keywords="['entrance', 'welcome', 'landing', 'front', 'home', 'page', 'style']">
|
||||
<MkRadios
|
||||
v-model="entrancePageStyle"
|
||||
:options="[
|
||||
{ value: 'classic' },
|
||||
{ value: 'simple' },
|
||||
]"
|
||||
>
|
||||
<MkRadios v-model="entrancePageStyle">
|
||||
<template #label><SearchLabel>{{ i18n.ts._serverSettings.entrancePageStyle }}</SearchLabel></template>
|
||||
<option value="classic">Classic</option>
|
||||
<option value="simple">Simple</option>
|
||||
</MkRadios>
|
||||
</SearchMarker>
|
||||
|
||||
@@ -155,8 +151,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import JSON5 from 'json5';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { host } from '@@/js/config.js';
|
||||
import type { ClientOptions } from '@/instance.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import * as os from '@/os.js';
|
||||
@@ -172,11 +168,11 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
const entrancePageStyle = ref<Misskey.entities.MetaClientOptions['entrancePageStyle']>(meta.clientOptions.entrancePageStyle ?? 'classic');
|
||||
const entrancePageStyle = ref<ClientOptions['entrancePageStyle']>(meta.clientOptions.entrancePageStyle ?? 'classic');
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
const showTimelineForVisitor = ref<Misskey.entities.MetaClientOptions['showTimelineForVisitor']>(meta.clientOptions.showTimelineForVisitor ?? true);
|
||||
const showTimelineForVisitor = ref<ClientOptions['showTimelineForVisitor']>(meta.clientOptions.showTimelineForVisitor ?? true);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
const showActivitiesForVisitor = ref<Misskey.entities.MetaClientOptions['showActivitiesForVisitor']>(meta.clientOptions.showActivitiesForVisitor ?? true);
|
||||
const showActivitiesForVisitor = ref<ClientOptions['showActivitiesForVisitor']>(meta.clientOptions.showActivitiesForVisitor ?? true);
|
||||
|
||||
const iconUrl = ref(meta.iconUrl);
|
||||
const app192IconUrl = ref(meta.app192IconUrl);
|
||||
@@ -195,11 +191,11 @@ const manifestJsonOverride = ref(meta.manifestJsonOverride === '' ? '{}' : JSON.
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
clientOptions: {
|
||||
clientOptions: ({
|
||||
entrancePageStyle: entrancePageStyle.value,
|
||||
showTimelineForVisitor: showTimelineForVisitor.value,
|
||||
showActivitiesForVisitor: showActivitiesForVisitor.value,
|
||||
},
|
||||
} as ClientOptions) as any,
|
||||
iconUrl: iconUrl.value,
|
||||
app192IconUrl: app192IconUrl.value,
|
||||
app512IconUrl: app512IconUrl.value,
|
||||
|
||||
@@ -25,15 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div class="_gaps_m">
|
||||
<div><SearchText>{{ i18n.ts._sensitiveMediaDetection.description }}</SearchText></div>
|
||||
|
||||
<MkRadios
|
||||
v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection"
|
||||
:options="[
|
||||
{ value: 'none', label: i18n.ts.none },
|
||||
{ value: 'all', label: i18n.ts.all },
|
||||
{ value: 'local', label: i18n.ts.localOnly },
|
||||
{ value: 'remote', label: i18n.ts.remoteOnly },
|
||||
]"
|
||||
>
|
||||
<MkRadios v-model="sensitiveMediaDetectionForm.state.sensitiveMediaDetection">
|
||||
<option value="none">{{ i18n.ts.none }}</option>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="local">{{ i18n.ts.localOnly }}</option>
|
||||
<option value="remote">{{ i18n.ts.remoteOnly }}</option>
|
||||
</MkRadios>
|
||||
|
||||
<SearchMarker :keywords="['sensitivity']">
|
||||
|
||||
@@ -21,8 +21,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #default="{ item, index, dragStart }">
|
||||
<div :class="$style.item">
|
||||
<div :class="$style.itemHeader">
|
||||
<div :class="$style.itemNumber">{{ index + 1 }}</div>
|
||||
<span :class="$style.itemHandle" :draggable="true" @dragstart.stop="dragStart"><i class="ti ti-menu"></i></span>
|
||||
<div :class="$style.itemNumber" v-text="String(index + 1)"/>
|
||||
<span :class="$style.itemHandle" :draggable="true" @dragstart.stop="dragStart"><i class="ti ti-menu"/></span>
|
||||
<button class="_button" :class="$style.itemRemove" @click="remove(item.id)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<MkInput :modelValue="item.text" @update:modelValue="serverRules[index].text = $event"/>
|
||||
|
||||
@@ -96,28 +96,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkFolder>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker v-slot="slotProps" :keywords="['features']">
|
||||
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
|
||||
<template #icon><SearchIcon><i class="ti ti-puzzle"></i></SearchIcon></template>
|
||||
<template #label><SearchLabel>{{ i18n.ts._serverSettings.features }}</SearchLabel></template>
|
||||
<template v-if="featuresForm.modified.value" #footer>
|
||||
<MkFormFooter :form="featuresForm"/>
|
||||
</template>
|
||||
|
||||
<div class="_gaps">
|
||||
<SearchMarker>
|
||||
<MkSwitch v-model="featuresForm.state.enableSpReaction">
|
||||
<template #label><SearchLabel>{{ i18n.ts._serverSettings._spReactions.enable }}</SearchLabel><span v-if="featuresForm.modifiedStates.enableSpReaction" class="_modified">{{ i18n.ts.modified }}</span></template>
|
||||
<template #caption>
|
||||
<SearchText>{{ i18n.ts._serverSettings._spReactions.description1 }}</SearchText>
|
||||
<div>{{ i18n.ts._serverSettings._spReactions.description2 }}</div>
|
||||
</template>
|
||||
</MkSwitch>
|
||||
</SearchMarker>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker v-slot="slotProps" :keywords="['pinned', 'users']">
|
||||
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
|
||||
<template #icon><SearchIcon><i class="ti ti-user-star"></i></SearchIcon></template>
|
||||
@@ -280,15 +258,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<div class="_gaps">
|
||||
<SearchMarker>
|
||||
<MkRadios
|
||||
v-model="federationForm.state.federation"
|
||||
:options="[
|
||||
{ value: 'all', label: i18n.ts.all },
|
||||
{ value: 'specified', label: i18n.ts.specifyHost },
|
||||
{ value: 'none', label: i18n.ts.none },
|
||||
]"
|
||||
>
|
||||
<MkRadios v-model="federationForm.state.federation">
|
||||
<template #label><SearchLabel>{{ i18n.ts.behavior }}</SearchLabel><span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
|
||||
<option value="all">{{ i18n.ts.all }}</option>
|
||||
<option value="specified">{{ i18n.ts.specifyHost }}</option>
|
||||
<option value="none">{{ i18n.ts.none }}</option>
|
||||
</MkRadios>
|
||||
</SearchMarker>
|
||||
|
||||
@@ -448,15 +422,6 @@ const infoForm = useForm({
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
||||
const featuresForm = useForm({
|
||||
enableSpReaction: meta.enableSpReaction,
|
||||
}, async (state) => {
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
enableSpReaction: state.enableSpReaction,
|
||||
});
|
||||
fetchInstance(true);
|
||||
});
|
||||
|
||||
const pinnedUsersForm = useForm({
|
||||
pinnedUsers: meta.pinnedUsers.join('\n'),
|
||||
}, async (state) => {
|
||||
|
||||
@@ -8,14 +8,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ entity.name || entity.url }}</template>
|
||||
<template v-if="entity.name != null && entity.name != ''" #caption>{{ entity.url }}</template>
|
||||
<template #icon>
|
||||
<i v-if="!entity.isActive" class="ti ti-player-pause"></i>
|
||||
<i v-else-if="entity.latestStatus === null" class="ti ti-circle"></i>
|
||||
<i v-if="!entity.isActive" class="ti ti-player-pause"/>
|
||||
<i v-else-if="entity.latestStatus === null" class="ti ti-circle"/>
|
||||
<i
|
||||
v-else-if="[200, 201, 204].includes(entity.latestStatus)"
|
||||
class="ti ti-check"
|
||||
:style="{ color: 'var(--MI_THEME-success)' }"
|
||||
></i>
|
||||
<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"></i>
|
||||
/>
|
||||
<i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"/>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/>
|
||||
|
||||
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<div v-if="permissions.length > 0">
|
||||
<div v-if="app.permission.length > 0">
|
||||
<p>{{ i18n.tsx._auth.permission({ name }) }}</p>
|
||||
<ul>
|
||||
<li v-for="p in permissions" :key="p">{{ i18n.ts._permissions[p] ?? p }}</li>
|
||||
<li v-for="p in app.permission" :key="p">{{ (i18n.ts._permissions as any)[p] ?? p }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>{{ i18n.tsx._auth.shareAccess({ name: `${name} (${app.id})` }) }}</div>
|
||||
@@ -37,10 +37,6 @@ const emit = defineEmits<{
|
||||
|
||||
const app = computed(() => props.session.app);
|
||||
|
||||
const permissions = computed(() => {
|
||||
return props.session.app.permission.filter((p): p is typeof Misskey.permissions[number] => typeof p === 'string');
|
||||
});
|
||||
|
||||
const name = computed(() => {
|
||||
const el = window.document.createElement('div');
|
||||
el.textContent = app.value.name;
|
||||
|
||||
@@ -11,14 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
<MkRadios
|
||||
v-model="searchType"
|
||||
:options="[
|
||||
{ value: 'nameAndDescription', label: i18n.ts._channel.nameAndDescription },
|
||||
{ value: 'nameOnly', label: i18n.ts._channel.nameOnly },
|
||||
]"
|
||||
@update:modelValue="search()"
|
||||
>
|
||||
<MkRadios v-model="searchType" @update:modelValue="search()">
|
||||
<option value="nameAndDescription">{{ i18n.ts._channel.nameAndDescription }}</option>
|
||||
<option value="nameOnly">{{ i18n.ts._channel.nameOnly }}</option>
|
||||
</MkRadios>
|
||||
<MkButton large primary gradate rounded @click="search">{{ i18n.ts.search }}</MkButton>
|
||||
</div>
|
||||
@@ -77,17 +72,15 @@ import { Paginator } from '@/utility/paginator.js';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
type SearchType = 'nameAndDescription' | 'nameOnly';
|
||||
|
||||
const props = defineProps<{
|
||||
query: string;
|
||||
type?: SearchType;
|
||||
type?: string;
|
||||
}>();
|
||||
|
||||
const key = ref('');
|
||||
const tab = ref('featured');
|
||||
const searchQuery = ref('');
|
||||
const searchType = ref<SearchType>('nameAndDescription');
|
||||
const searchType = ref('nameAndDescription');
|
||||
const channelPaginator = shallowRef();
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -58,7 +58,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
|
||||
<img v-if="store.s.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
|
||||
<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
|
||||
<canvas ref="canvasEl" :class="$style.canvas"></canvas>
|
||||
<canvas ref="canvasEl" :class="$style.canvas"/>
|
||||
<Transition
|
||||
:enterActiveClass="$style.transition_combo_enterActive"
|
||||
:leaveActiveClass="$style.transition_combo_leaveActive"
|
||||
@@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</Transition>
|
||||
<template v-if="dropReady && currentPick">
|
||||
<img src="/client-assets/drop-and-fusion/drop-arrow.svg" :class="$style.currentMonoArrow"/>
|
||||
<div :class="$style.dropGuide"></div>
|
||||
<div :class="$style.dropGuide"/>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="isGameOver && !replaying" :class="$style.gameOverLabel">
|
||||
|
||||
@@ -212,7 +212,7 @@ async function run() {
|
||||
const version = utils.getLangVersion(flash.value.script);
|
||||
const isLegacy = getIsLegacy(version);
|
||||
|
||||
const { Interpreter, Parser, values } = (isLegacy ? (await import('@syuilo/aiscript-0-19-0')) : await import('@syuilo/aiscript')) as typeof import('@syuilo/aiscript');
|
||||
const { Interpreter, Parser, values } = isLegacy ? (await import('@syuilo/aiscript-0-19-0') as any) : await import('@syuilo/aiscript');
|
||||
|
||||
const parser = new Parser();
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<p class="acct">@{{ acct(displayUser(req)) }}</p>
|
||||
</div>
|
||||
<div v-if="tab === 'list'" class="commands">
|
||||
<MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"></i> {{ i18n.ts.accept }}</MkButton>
|
||||
<MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"></i> {{ i18n.ts.reject }}</MkButton>
|
||||
<MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
|
||||
<MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
||||
</div>
|
||||
<div v-else class="commands">
|
||||
<MkButton class="command" rounded danger @click="cancel(displayUser(req))"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
|
||||
<MkButton class="command" rounded danger @click="cancel(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.cancel }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -151,7 +151,7 @@ import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
@@ -160,6 +160,8 @@ import * as os from '@/os.js';
|
||||
import { confetti } from '@/utility/confetti.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const props = defineProps<{
|
||||
game: Misskey.entities.ReversiGameDetailed;
|
||||
connection?: Misskey.IChannelConnection<Misskey.Channels['reversiGame']> | null;
|
||||
@@ -180,13 +182,13 @@ const engine = shallowRef<Reversi.Game>(Reversi.Serializer.restoreGame({
|
||||
}));
|
||||
|
||||
const iAmPlayer = computed(() => {
|
||||
return game.value.user1Id === $i?.id || game.value.user2Id === $i?.id;
|
||||
return game.value.user1Id === $i.id || game.value.user2Id === $i.id;
|
||||
});
|
||||
|
||||
const myColor = computed(() => {
|
||||
if (!iAmPlayer.value) return null;
|
||||
if (game.value.user1Id === $i?.id && game.value.black === 1) return true;
|
||||
if (game.value.user2Id === $i?.id && game.value.black === 2) return true;
|
||||
if (game.value.user1Id === $i.id && game.value.black === 1) return true;
|
||||
if (game.value.user2Id === $i.id && game.value.black === 2) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -217,7 +219,7 @@ const isMyTurn = computed(() => {
|
||||
if (!iAmPlayer.value) return false;
|
||||
const u = turnUser.value;
|
||||
if (u == null) return false;
|
||||
return u.id === $i?.id;
|
||||
return u.id === $i.id;
|
||||
});
|
||||
|
||||
const cellsStyle = computed(() => {
|
||||
@@ -352,7 +354,7 @@ function onStreamEnded(x: {
|
||||
}) {
|
||||
game.value = deepClone(x.game);
|
||||
|
||||
if (game.value.winnerId === $i?.id) {
|
||||
if (game.value.winnerId === $i.id) {
|
||||
confetti({
|
||||
duration: 1000 * 3,
|
||||
});
|
||||
|
||||
@@ -35,28 +35,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkFolder :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts._reversi.blackOrWhite }}</template>
|
||||
|
||||
<MkRadios
|
||||
v-model="game.bw"
|
||||
:options="[
|
||||
{ value: 'random', label: i18n.ts.random },
|
||||
{ value: '1', slotId: 'user1' },
|
||||
{ value: '2', slotId: 'user2' },
|
||||
]"
|
||||
>
|
||||
<template #option-user1>
|
||||
<MkRadios v-model="game.bw">
|
||||
<option value="random">{{ i18n.ts.random }}</option>
|
||||
<option :value="'1'">
|
||||
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
||||
<template #name>
|
||||
<b><MkUserName :user="game.user1"/></b>
|
||||
</template>
|
||||
</I18n>
|
||||
</template>
|
||||
<template #option-user2>
|
||||
</option>
|
||||
<option :value="'2'">
|
||||
<I18n :src="i18n.ts._reversi.blackIs" tag="span">
|
||||
<template #name>
|
||||
<b><MkUserName :user="game.user2"/></b>
|
||||
</template>
|
||||
</I18n>
|
||||
</template>
|
||||
</option>
|
||||
</MkRadios>
|
||||
</MkFolder>
|
||||
|
||||
@@ -64,10 +58,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #label>{{ i18n.ts._reversi.timeLimitForEachTurn }}</template>
|
||||
<template #suffix>{{ game.timeLimitForEachTurn }}{{ i18n.ts._time.second }}</template>
|
||||
|
||||
<MkRadios
|
||||
v-model="game.timeLimitForEachTurn"
|
||||
:options="gameTurnOptionsDef"
|
||||
>
|
||||
<MkRadios v-model="game.timeLimitForEachTurn">
|
||||
<option :value="5">5{{ i18n.ts._time.second }}</option>
|
||||
<option :value="10">10{{ i18n.ts._time.second }}</option>
|
||||
<option :value="30">30{{ i18n.ts._time.second }}</option>
|
||||
<option :value="60">60{{ i18n.ts._time.second }}</option>
|
||||
<option :value="90">90{{ i18n.ts._time.second }}</option>
|
||||
<option :value="120">120{{ i18n.ts._time.second }}</option>
|
||||
<option :value="180">180{{ i18n.ts._time.second }}</option>
|
||||
<option :value="3600">3600{{ i18n.ts._time.second }}</option>
|
||||
</MkRadios>
|
||||
</MkFolder>
|
||||
|
||||
@@ -111,21 +110,22 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch, ref, onUnmounted } from 'vue';
|
||||
import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import * as Reversi from 'misskey-reversi';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import * as os from '@/os.js';
|
||||
import type { MkRadiosOption } from '@/components/MkRadios.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.category)));
|
||||
@@ -139,30 +139,19 @@ const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false }
|
||||
|
||||
const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
|
||||
|
||||
const gameTurnOptionsDef = [
|
||||
{ value: 5, label: '5' + i18n.ts._time.second },
|
||||
{ value: 10, label: '10' + i18n.ts._time.second },
|
||||
{ value: 30, label: '30' + i18n.ts._time.second },
|
||||
{ value: 60, label: '60' + i18n.ts._time.second },
|
||||
{ value: 90, label: '90' + i18n.ts._time.second },
|
||||
{ value: 120, label: '120' + i18n.ts._time.second },
|
||||
{ value: 180, label: '180' + i18n.ts._time.second },
|
||||
{ value: 3600, label: '3600' + i18n.ts._time.second },
|
||||
] as MkRadiosOption<number>[];
|
||||
|
||||
const mapName = computed(() => {
|
||||
if (game.value.map == null) return 'Random';
|
||||
const found = Object.values(Reversi.maps).find(x => x.data.join('') === game.value.map.join(''));
|
||||
return found ? found.name! : '-Custom-';
|
||||
});
|
||||
const isReady = computed(() => {
|
||||
if (game.value.user1Id === $i?.id && game.value.user1Ready) return true;
|
||||
if (game.value.user2Id === $i?.id && game.value.user2Ready) return true;
|
||||
if (game.value.user1Id === $i.id && game.value.user1Ready) return true;
|
||||
if (game.value.user2Id === $i.id && game.value.user2Ready) return true;
|
||||
return false;
|
||||
});
|
||||
const isOpReady = computed(() => {
|
||||
if (game.value.user1Id !== $i?.id && game.value.user1Ready) return true;
|
||||
if (game.value.user2Id !== $i?.id && game.value.user2Ready) return true;
|
||||
if (game.value.user1Id !== $i.id && game.value.user1Ready) return true;
|
||||
if (game.value.user2Id !== $i.id && game.value.user2Ready) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
@@ -239,7 +228,7 @@ function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) {
|
||||
}
|
||||
|
||||
function onUpdateSettings<K extends typeof Misskey.reversiUpdateKeys[number]>({ userId, key, value }: { userId: string; key: K; value: Misskey.entities.ReversiGameDetailed[K]; }) {
|
||||
if (userId === $i?.id) return;
|
||||
if (userId === $i.id) return;
|
||||
if (game.value[key] === value) return;
|
||||
game.value[key] = value;
|
||||
if (isReady.value) {
|
||||
|
||||
@@ -17,13 +17,15 @@ import GameBoard from './game.board.vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { $i } from '@/i.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import * as os from '@/os.js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -72,7 +74,7 @@ async function fetchGame() {
|
||||
connection.value.on('canceled', x => {
|
||||
connection.value?.dispose();
|
||||
|
||||
if (x.userId !== $i?.id) {
|
||||
if (x.userId !== $i.id) {
|
||||
os.alert({
|
||||
type: 'warning',
|
||||
text: i18n.ts._reversi.gameCanceled,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user