forked from FoundKeyGang/FoundKey
Merge branch 'develop' into mk.absturztau.be
This commit is contained in:
commit
ab0f2dc922
77 changed files with 1808 additions and 1629 deletions
5
.github/labeler.yml
vendored
Normal file
5
.github/labeler.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
'⚙️Server':
|
||||
- packages/backend/**/*
|
||||
|
||||
'🖥️Client':
|
||||
- packages/client/**/*
|
14
.github/workflows/labeler.yml
vendored
Normal file
14
.github/workflows/labeler.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -18,6 +18,14 @@ You should also include the user name that made the change.
|
|||
- enhance: API: notifications/readは配列でも受け付けるように #7667 @tamaina
|
||||
- enhance: プッシュ通知を複数アカウント対応に #7667 @tamaina
|
||||
- enhance: プッシュ通知にクリックやactionを設定 #7667 @tamaina
|
||||
- replaced webpack with Vite @tamaina
|
||||
- update dependencies @syuilo
|
||||
- enhance: display URL of QR code for TOTP registration @syuilo
|
||||
- make CAPTCHA required for signin to improve security @syuilo
|
||||
- The theme color is now better validated. @Johann150
|
||||
Your own theme color may be unset if it was in an invalid format.
|
||||
Admins should check their instance settings if in doubt.
|
||||
- Perform port diagnosis at startup only when Listen fails @mei23
|
||||
|
||||
### Bugfixes
|
||||
- Client: fix settings page @tamaina
|
||||
|
@ -25,6 +33,14 @@ You should also include the user name that made the change.
|
|||
- Server: await promises when following or unfollowing users @Johann150
|
||||
- Client: fix abuse reports page to be able to show all reports @Johann150
|
||||
- Federation: Add rel attribute to host-meta @mei23
|
||||
- Client: fix profile picture height in mentions @tamaina
|
||||
- MFM: more animated functions support `speed` parameter @futchitwo
|
||||
- Federation: Fix quote renotes containing no text being federated correctly @Johann150
|
||||
- Server: fix missing foreign key for reports leading to reports page being unusable @Johann150
|
||||
- Server: fix internal in-memory caching @Johann150
|
||||
- Server: use correct order of attachments on notes @Johann150
|
||||
- Server: prevent crash when processing certain PNGs @syuilo
|
||||
- Server: Fix unable to generate video thumbnails @mei23
|
||||
|
||||
## 12.110.1 (2022/04/23)
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ Before creating an issue, please check the following:
|
|||
- Do not use Issues to ask questions or troubleshooting.
|
||||
- Issues should only be used to feature requests, suggestions, and bug tracking.
|
||||
- Please ask questions or troubleshooting in the [Misskey Forum](https://forum.misskey.io/) or [Discord](https://discord.gg/Wp8gVStHW3).
|
||||
- Do not close issues that are about to be resolved. It should remain open until a commit that actually resolves it is merged.
|
||||
|
||||
## Before implementation
|
||||
When you want to add a feature or fix a bug, **first have the design and policy reviewed in an Issue** (if it is not there, please make one). Without this step, there is a high possibility that the PR will not be merged even if it is implemented.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
FROM node:18.0.0-alpine3.15 AS base
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ARG NODE_ENV=production
|
||||
|
||||
WORKDIR /misskey
|
||||
|
||||
|
@ -31,5 +31,6 @@ COPY --from=builder /misskey/packages/backend/built ./packages/backend/built
|
|||
COPY --from=builder /misskey/packages/client/node_modules ./packages/client/node_modules
|
||||
COPY . ./
|
||||
|
||||
ENV NODE_ENV=production
|
||||
CMD ["npm", "run", "migrateandstart"]
|
||||
|
||||
|
|
52
README.md
52
README.md
|
@ -1,27 +1,32 @@
|
|||
[![Misskey](https://github.com/misskey-dev/assets/blob/main/banner.png?raw=true)](https://join.misskey.page/)
|
||||
|
||||
<div align="center">
|
||||
<a href="https://misskey-hub.net">
|
||||
<img src="./assets/title_float.svg" alt="Misskey logo" style="border-radius:50%" width="400"/>
|
||||
</a>
|
||||
|
||||
**🌎 A forever evolving, interplanetary microblogging platform. 🚀**
|
||||
|
||||
**Misskey** is a distributed microblogging platform with advanced features such as Reactions and a highly customizable UI.
|
||||
|
||||
[Learn more](https://misskey-hub.net/)
|
||||
**🌎 **[Misskey](https://misskey-hub.net/)** is an open source, decentralized social media platform that's free forever! 🚀**
|
||||
|
||||
---
|
||||
|
||||
[✨ Find an instance](https://misskey-hub.net/instances.html)
|
||||
•
|
||||
[📦 Create your own instance](https://misskey-hub.net/docs/install.html)
|
||||
•
|
||||
[🛠️ Contribute](./CONTRIBUTING.md)
|
||||
•
|
||||
[🚀 Join the community](https://discord.gg/Wp8gVStHW3)
|
||||
<a href="https://misskey-hub.net/instances.html">
|
||||
<img src="https://custom-icon-badges.herokuapp.com/badge/find_an-instance-acea31?logoColor=acea31&style=for-the-badge&logo=misskey&labelColor=363B40" alt="find an instance"/></a>
|
||||
|
||||
<a href="https://misskey-hub.net/docs/install.html">
|
||||
<img src="https://custom-icon-badges.herokuapp.com/badge/create_an-instance-FBD53C?logoColor=FBD53C&style=for-the-badge&logo=server&labelColor=363B40" alt="create an instance"/></a>
|
||||
|
||||
<a href="./CONTRIBUTING.md">
|
||||
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-contributor-A371F7?logoColor=A371F7&style=for-the-badge&logo=git-merge&labelColor=363B40" alt="become a contributor"/>
|
||||
</a>
|
||||
|
||||
<a href="https://discord.gg/Wp8gVStHW3">
|
||||
<img src="https://custom-icon-badges.herokuapp.com/badge/join_the-community-5865F2?logoColor=5865F2&style=for-the-badge&logo=discord&labelColor=363B40" alt="join the community"/>
|
||||
</a>
|
||||
|
||||
<a href="https://www.patreon.com/syuilo">
|
||||
<img src="https://custom-icon-badges.herokuapp.com/badge/become_a-patron-F96854?logoColor=F96854&style=for-the-badge&logo=patreon&labelColor=363B40" alt="become a patron"/>
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@ -30,17 +35,16 @@
|
|||
|
||||
## ✨ Features
|
||||
- **ActivityPub support**\
|
||||
It is possible to interact with other software.
|
||||
Not on Misskey? No problem! Not only can Misskey instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed!
|
||||
- **Reactions**\
|
||||
You can add "reactions" to each post, making it easy for you to express your feelings.
|
||||
You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button.
|
||||
- **Drive**\
|
||||
An interface to manage uploaded files such as images, videos, sounds, etc.
|
||||
You can also organize your favorite content into folders, making it easy to share again.
|
||||
With Misskey's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made!
|
||||
- **Rich Web UI**\
|
||||
Misskey has a rich WebUI by default.
|
||||
It is highly customizable by flexibly changing the layout and installing various widgets and themes.
|
||||
Furthermore, plug-ins can be created using AiScript, a original programming language.
|
||||
- and more...
|
||||
Misskey has a rich and easy to use Web UI!
|
||||
It is highly customizable, from changing the layout and adding widgets to making custom themes.
|
||||
Furthermore, plugins can be created using AiScript, an original programming language.
|
||||
- And much more...
|
||||
|
||||
</div>
|
||||
|
||||
|
|
BIN
assets/title_float.svg
Normal file
BIN
assets/title_float.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.1 KiB |
3
chart/Chart.yaml
Normal file
3
chart/Chart.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
apiVersion: v2
|
||||
name: misskey
|
||||
version: 0.0.0
|
165
chart/files/default.yml
Normal file
165
chart/files/default.yml
Normal file
|
@ -0,0 +1,165 @@
|
|||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
# Misskey configuration
|
||||
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
# ┌─────┐
|
||||
#───┘ URL └─────────────────────────────────────────────────────
|
||||
|
||||
# Final accessible URL seen by a user.
|
||||
# url: https://example.tld/
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# URL SETTINGS AFTER THAT!
|
||||
|
||||
# ┌───────────────────────┐
|
||||
#───┘ Port and TLS settings └───────────────────────────────────
|
||||
|
||||
#
|
||||
# Misskey supports two deployment options for public.
|
||||
#
|
||||
|
||||
# Option 1: With Reverse Proxy
|
||||
#
|
||||
# +----- https://example.tld/ ------------+
|
||||
# +------+ |+-------------+ +----------------+|
|
||||
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
|
||||
# +------+ |+-------------+ +----------------+|
|
||||
# +---------------------------------------+
|
||||
#
|
||||
# You need to setup reverse proxy. (eg. nginx)
|
||||
# You do not define 'https' section.
|
||||
|
||||
# Option 2: Standalone
|
||||
#
|
||||
# +- https://example.tld/ -+
|
||||
# +------+ | +---------------+ |
|
||||
# | User | ---> | | Misskey (443) | |
|
||||
# +------+ | +---------------+ |
|
||||
# +------------------------+
|
||||
#
|
||||
# You need to run Misskey as root.
|
||||
# You need to set Certificate in 'https' section.
|
||||
|
||||
# To use option 1, uncomment below line.
|
||||
port: 3000 # A port that your Misskey server should listen.
|
||||
|
||||
# To use option 2, uncomment below lines.
|
||||
#port: 443
|
||||
|
||||
#https:
|
||||
# # path for certification
|
||||
# key: /etc/letsencrypt/live/example.tld/privkey.pem
|
||||
# cert: /etc/letsencrypt/live/example.tld/fullchain.pem
|
||||
|
||||
# ┌──────────────────────────┐
|
||||
#───┘ PostgreSQL configuration └────────────────────────────────
|
||||
|
||||
db:
|
||||
host: localhost
|
||||
port: 5432
|
||||
|
||||
# Database name
|
||||
db: misskey
|
||||
|
||||
# Auth
|
||||
user: example-misskey-user
|
||||
pass: example-misskey-pass
|
||||
|
||||
# Whether disable Caching queries
|
||||
#disableCache: true
|
||||
|
||||
# Extra Connection options
|
||||
#extra:
|
||||
# ssl: true
|
||||
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Redis configuration └─────────────────────────────────────
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
#pass: example-pass
|
||||
#prefix: example-prefix
|
||||
#db: 1
|
||||
|
||||
# ┌─────────────────────────────┐
|
||||
#───┘ Elasticsearch configuration └─────────────────────────────
|
||||
|
||||
#elasticsearch:
|
||||
# host: localhost
|
||||
# port: 9200
|
||||
# ssl: false
|
||||
# user:
|
||||
# pass:
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
|
||||
# You can select the ID generation method.
|
||||
# You don't usually need to change this setting, but you can
|
||||
# change it according to your preferences.
|
||||
|
||||
# Available methods:
|
||||
# aid ... Short, Millisecond accuracy
|
||||
# meid ... Similar to ObjectID, Millisecond accuracy
|
||||
# ulid ... Millisecond accuracy
|
||||
# objectid ... This is left for backward compatibility
|
||||
|
||||
# ONCE YOU HAVE STARTED THE INSTANCE, DO NOT CHANGE THE
|
||||
# ID SETTINGS AFTER THAT!
|
||||
|
||||
id: "aid"
|
||||
# ┌─────────────────────┐
|
||||
#───┘ Other configuration └─────────────────────────────────────
|
||||
|
||||
# Whether disable HSTS
|
||||
#disableHsts: true
|
||||
|
||||
# Number of worker processes
|
||||
#clusterLimit: 1
|
||||
|
||||
# Job concurrency per worker
|
||||
# deliverJobConcurrency: 128
|
||||
# inboxJobConcurrency: 16
|
||||
|
||||
# Job rate limiter
|
||||
# deliverJobPerSec: 128
|
||||
# inboxJobPerSec: 16
|
||||
|
||||
# Job attempts
|
||||
# deliverJobMaxAttempts: 12
|
||||
# inboxJobMaxAttempts: 8
|
||||
|
||||
# IP address family used for outgoing request (ipv4, ipv6 or dual)
|
||||
#outgoingAddressFamily: ipv4
|
||||
|
||||
# Syslog option
|
||||
#syslog:
|
||||
# host: localhost
|
||||
# port: 514
|
||||
|
||||
# Proxy for HTTP/HTTPS
|
||||
#proxy: http://127.0.0.1:3128
|
||||
|
||||
#proxyBypassHosts: [
|
||||
# 'example.com',
|
||||
# '192.0.2.8'
|
||||
#]
|
||||
|
||||
# Proxy for SMTP/SMTPS
|
||||
#proxySmtp: http://127.0.0.1:3128 # use HTTP/1.1 CONNECT
|
||||
#proxySmtp: socks4://127.0.0.1:1080 # use SOCKS4
|
||||
#proxySmtp: socks5://127.0.0.1:1080 # use SOCKS5
|
||||
|
||||
# Media Proxy
|
||||
#mediaProxy: https://example.com/proxy
|
||||
|
||||
# Sign to ActivityPub GET request (default: false)
|
||||
#signToActivityPubGet: true
|
||||
|
||||
#allowedPrivateNetworks: [
|
||||
# '127.0.0.1/32'
|
||||
#]
|
||||
|
||||
# Upload or download file size limits (bytes)
|
||||
#maxFileSize: 262144000
|
8
chart/templates/ConfigMap.yml
Normal file
8
chart/templates/ConfigMap.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "misskey.fullname" . }}-configuration
|
||||
data:
|
||||
default.yml: |-
|
||||
{{ .Files.Get "files/default.yml"|nindent 4 }}
|
||||
url: {{ .Values.url }}
|
47
chart/templates/Deployment.yml
Normal file
47
chart/templates/Deployment.yml
Normal file
|
@ -0,0 +1,47 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "misskey.fullname" . }}
|
||||
labels:
|
||||
{{- include "misskey.labels" . | nindent 4 }}
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "misskey.selectorLabels" . | nindent 6 }}
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "misskey.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
containers:
|
||||
- name: misskey
|
||||
image: {{ .Values.image }}
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: {{ .Values.environment }}
|
||||
volumeMounts:
|
||||
- name: {{ include "misskey.fullname" . }}-configuration
|
||||
mountPath: /misskey/.config
|
||||
readOnly: true
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
- name: postgres
|
||||
image: postgres:14-alpine
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: "example-misskey-user"
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: "example-misskey-pass"
|
||||
- name: POSTGRES_DB
|
||||
value: "misskey"
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
- name: redis
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
volumes:
|
||||
- name: {{ include "misskey.fullname" . }}-configuration
|
||||
configMap:
|
||||
name: {{ include "misskey.fullname" . }}-configuration
|
14
chart/templates/Service.yml
Normal file
14
chart/templates/Service.yml
Normal file
|
@ -0,0 +1,14 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "misskey.fullname" . }}
|
||||
annotations:
|
||||
dev.okteto.com/auto-ingress: "true"
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 3000
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "misskey.selectorLabels" . | nindent 4 }}
|
62
chart/templates/_helpers.tpl
Normal file
62
chart/templates/_helpers.tpl
Normal file
|
@ -0,0 +1,62 @@
|
|||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "misskey.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "misskey.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "misskey.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "misskey.labels" -}}
|
||||
helm.sh/chart: {{ include "misskey.chart" . }}
|
||||
{{ include "misskey.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "misskey.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "misskey.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "misskey.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "misskey.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
3
chart/values.yml
Normal file
3
chart/values.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
url: https://example.tld/
|
||||
image: okteto.dev/misskey
|
||||
environment: production
|
|
@ -9,7 +9,7 @@ services:
|
|||
- redis
|
||||
# - es
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
- "3000:3000"
|
||||
networks:
|
||||
- internal_network
|
||||
- external_network
|
||||
|
|
|
@ -425,7 +425,7 @@ quoteQuestion: "引用として添付しますか?"
|
|||
noMessagesYet: "まだチャットはありません"
|
||||
newMessageExists: "新しいメッセージがあります"
|
||||
onlyOneFileCanBeAttached: "メッセージに添付できるファイルはひとつです"
|
||||
signinRequired: "ログインしてください"
|
||||
signinRequired: "続行する前に、サインアップまたはサインインが必要です"
|
||||
invitations: "招待"
|
||||
invitationCode: "招待コード"
|
||||
checking: "確認しています"
|
||||
|
|
6
okteto.yml
Normal file
6
okteto.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
build:
|
||||
misskey:
|
||||
args:
|
||||
- NODE_ENV=development
|
||||
deploy:
|
||||
- helm upgrade --install misskey chart --set image=${OKTETO_BUILD_MISSKEY_IMAGE} --set url="https://misskey-$(kubectl config view --minify -o jsonpath='{..namespace}').cloud.okteto.net" --set environment=development
|
|
@ -0,0 +1,36 @@
|
|||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export class uniformThemecolor1652859567549 {
|
||||
name = 'uniformThemecolor1652859567549'
|
||||
|
||||
async up(queryRunner) {
|
||||
const formatColor = (color) => {
|
||||
let tc = new tinycolor(color);
|
||||
if (tc.isValid()) {
|
||||
return tc.toHexString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
await queryRunner.query('SELECT "id", "themeColor" FROM "instance" WHERE "themeColor" IS NOT NULL')
|
||||
.then(instances => Promise.all(instances.map(instance => {
|
||||
// update theme color to uniform format, e.g. #00ff00
|
||||
// invalid theme colors get set to null
|
||||
return queryRunner.query('UPDATE "instance" SET "themeColor" = $1 WHERE "id" = $2', [formatColor(instance.themeColor), instance.id]);
|
||||
})));
|
||||
|
||||
// also fix own theme color
|
||||
await queryRunner.query('SELECT "themeColor" FROM "meta" WHERE "themeColor" IS NOT NULL LIMIT 1')
|
||||
.then(metas => {
|
||||
if (metas.length > 0) {
|
||||
return queryRunner.query('UPDATE "meta" SET "themeColor" = $1', [formatColor(metas[0].themeColor)]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
// The original representation is not stored, so migrating back is not possible.
|
||||
// The new format also works in older versions so this is not a problem.
|
||||
}
|
||||
}
|
|
@ -77,7 +77,6 @@
|
|||
"os-utils": "0.0.14",
|
||||
"parse5": "6.0.1",
|
||||
"pg": "8.7.3",
|
||||
"portscanner": "2.2.0",
|
||||
"private-ip": "2.3.3",
|
||||
"probe-image-size": "7.2.3",
|
||||
"promise-limit": "2.7.0",
|
||||
|
@ -97,7 +96,7 @@
|
|||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.7.0",
|
||||
"semver": "7.3.7",
|
||||
"sharp": "0.30.4",
|
||||
"sharp": "0.29.3",
|
||||
"speakeasy": "2.0.0",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
|
@ -151,7 +150,6 @@
|
|||
"@types/nodemailer": "6.4.4",
|
||||
"@types/oauth": "0.9.1",
|
||||
"@types/parse5": "6.0.3",
|
||||
"@types/portscanner": "2.1.1",
|
||||
"@types/pug": "2.0.6",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/qrcode": "1.4.2",
|
||||
|
|
|
@ -5,7 +5,6 @@ import * as os from 'node:os';
|
|||
import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
import chalkTemplate from 'chalk-template';
|
||||
import * as portscanner from 'portscanner';
|
||||
import semver from 'semver';
|
||||
|
||||
import Logger from '@/services/logger.js';
|
||||
|
@ -48,11 +47,6 @@ function greet() {
|
|||
bootLogger.info(`Misskey v${meta.version}`, null, true);
|
||||
}
|
||||
|
||||
function isRoot() {
|
||||
// maybe process.getuid will be undefined under not POSIX environment (e.g. Windows)
|
||||
return process.getuid != null && process.getuid() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init master process
|
||||
*/
|
||||
|
@ -67,7 +61,6 @@ export async function masterMain() {
|
|||
showNodejsVersion();
|
||||
config = loadConfigBoot();
|
||||
await connectDb();
|
||||
await validatePort(config);
|
||||
} catch (e) {
|
||||
bootLogger.error('Fatal error occurred during initialization', null, true);
|
||||
process.exit(1);
|
||||
|
@ -97,8 +90,6 @@ function showEnvironment(): void {
|
|||
logger.warn('The environment is not in production mode.');
|
||||
logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
|
||||
}
|
||||
|
||||
logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
|
||||
}
|
||||
|
||||
function showNodejsVersion(): void {
|
||||
|
@ -152,29 +143,6 @@ async function connectDb(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
async function validatePort(config: Config): Promise<void> {
|
||||
const isWellKnownPort = (port: number) => port < 1024;
|
||||
|
||||
async function isPortAvailable(port: number): Promise<boolean> {
|
||||
return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
|
||||
}
|
||||
|
||||
if (config.port == null || Number.isNaN(config.port)) {
|
||||
bootLogger.error('The port is not configured. Please configure port.', null, true);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
|
||||
bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!await isPortAvailable(config.port)) {
|
||||
bootLogger.error(`Port ${config.port} is already in use`, null, true);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function spawnWorkers(limit: number = 1) {
|
||||
const workers = Math.min(limit, os.cpus().length);
|
||||
bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
|
||||
|
@ -186,6 +154,10 @@ function spawnWorker(): Promise<void> {
|
|||
return new Promise(res => {
|
||||
const worker = cluster.fork();
|
||||
worker.on('message', message => {
|
||||
if (message === 'listenFailed') {
|
||||
bootLogger.error(`The server Listen failed due to the previous error.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (message !== 'ready') return;
|
||||
res();
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
|
|||
import { getApLock } from '@/misc/app-lock.js';
|
||||
import { parseAudience } from '../../audience.js';
|
||||
import { StatusError } from '@/misc/fetch.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
|
@ -52,6 +53,8 @@ export default async function(resolver: Resolver, actor: CacheableRemoteUser, ac
|
|||
throw e;
|
||||
}
|
||||
|
||||
if (!await Notes.isVisibleForMe(renote, actor)) return 'skip: invalid actor for this activity';
|
||||
|
||||
logger.info(`Creating the (Re)Note: ${uri}`);
|
||||
|
||||
const activityAudience = await parseAudience(actor, activity.to, activity.cc);
|
||||
|
|
|
@ -13,37 +13,37 @@ export default async (actor: CacheableRemoteUser, activity: IDelete): Promise<st
|
|||
}
|
||||
|
||||
// 削除対象objectのtype
|
||||
let formarType: string | undefined;
|
||||
let formerType: string | undefined;
|
||||
|
||||
if (typeof activity.object === 'string') {
|
||||
// typeが不明だけど、どうせ消えてるのでremote resolveしない
|
||||
formarType = undefined;
|
||||
formerType = undefined;
|
||||
} else {
|
||||
const object = activity.object as IObject;
|
||||
if (isTombstone(object)) {
|
||||
formarType = toSingle(object.formerType);
|
||||
formerType = toSingle(object.formerType);
|
||||
} else {
|
||||
formarType = toSingle(object.type);
|
||||
formerType = toSingle(object.type);
|
||||
}
|
||||
}
|
||||
|
||||
const uri = getApId(activity.object);
|
||||
|
||||
// type不明でもactorとobjectが同じならばそれはPersonに違いない
|
||||
if (!formarType && actor.uri === uri) {
|
||||
formarType = 'Person';
|
||||
if (!formerType && actor.uri === uri) {
|
||||
formerType = 'Person';
|
||||
}
|
||||
|
||||
// それでもなかったらおそらくNote
|
||||
if (!formarType) {
|
||||
formarType = 'Note';
|
||||
if (!formerType) {
|
||||
formerType = 'Note';
|
||||
}
|
||||
|
||||
if (validPost.includes(formarType)) {
|
||||
if (validPost.includes(formerType)) {
|
||||
return await deleteNote(actor, uri);
|
||||
} else if (validActor.includes(formarType)) {
|
||||
} else if (validActor.includes(formerType)) {
|
||||
return await deleteActor(actor, uri);
|
||||
} else {
|
||||
return `Unknown type ${formarType}`;
|
||||
return `Unknown type ${formerType}`;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnoun
|
|||
|
||||
const note = await Notes.findOneBy({
|
||||
uri,
|
||||
userId: actor.id,
|
||||
});
|
||||
|
||||
if (!note) return 'skip: no such Announce';
|
||||
|
|
|
@ -27,7 +27,7 @@ export const paramDef = {
|
|||
blockedHosts: { type: 'array', nullable: true, items: {
|
||||
type: 'string',
|
||||
} },
|
||||
themeColor: { type: 'string', nullable: true },
|
||||
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
|
||||
mascotImageUrl: { type: 'string', nullable: true },
|
||||
bannerUrl: { type: 'string', nullable: true },
|
||||
errorImageUrl: { type: 'string', nullable: true },
|
||||
|
|
|
@ -177,7 +177,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
userId: user.id,
|
||||
fileIds,
|
||||
})
|
||||
.orderBy('array_position(ARRAY[:...fileIds], "id")')
|
||||
.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
|
||||
.setParameters({ fileIds })
|
||||
.getMany();
|
||||
}
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
import { randomBytes } from 'node:crypto';
|
||||
import Koa from 'koa';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import * as speakeasy from 'speakeasy';
|
||||
import signin from '../common/signin.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
import config from '@/config/index.js';
|
||||
import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js';
|
||||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js';
|
||||
import { verifyLogin, hash } from '../2fa.js';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { IsNull } from 'typeorm';
|
||||
import signin from '../common/signin.js';
|
||||
|
||||
export default async (ctx: Koa.Context) => {
|
||||
ctx.set('Access-Control-Allow-Origin', config.url);
|
||||
ctx.set('Access-Control-Allow-Credentials', 'true');
|
||||
|
||||
const body = ctx.request.body as any;
|
||||
|
||||
const instance = await fetchMeta(true);
|
||||
|
||||
const username = body['username'];
|
||||
const password = body['password'];
|
||||
const token = body['token'];
|
||||
|
@ -79,6 +84,18 @@ export default async (ctx: Koa.Context) => {
|
|||
}
|
||||
|
||||
if (!profile.twoFactorEnabled) {
|
||||
if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
|
||||
await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => {
|
||||
ctx.throw(400, e);
|
||||
});
|
||||
}
|
||||
|
||||
if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
|
||||
await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => {
|
||||
ctx.throw(400, e);
|
||||
});
|
||||
}
|
||||
|
||||
if (same) {
|
||||
signin(ctx, user);
|
||||
return;
|
||||
|
@ -155,7 +172,7 @@ export default async (ctx: Koa.Context) => {
|
|||
body.credentialId
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/'),
|
||||
'base64'
|
||||
'base64',
|
||||
).toString('hex'),
|
||||
});
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* Core Server
|
||||
*/
|
||||
|
||||
import cluster from 'node:cluster';
|
||||
import * as fs from 'node:fs';
|
||||
import * as http from 'node:http';
|
||||
import Koa from 'koa';
|
||||
|
@ -142,5 +143,26 @@ export default () => new Promise(resolve => {
|
|||
|
||||
initializeStreamingServer(server);
|
||||
|
||||
server.on('error', e => {
|
||||
switch ((e as any).code) {
|
||||
case 'EACCES':
|
||||
serverLogger.error(`You do not have permission to listen on port ${config.port}.`);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
serverLogger.error(`Port ${config.port} is already in use by another process.`);
|
||||
break;
|
||||
default:
|
||||
serverLogger.error(e);
|
||||
break;
|
||||
}
|
||||
|
||||
if (cluster.isWorker) {
|
||||
process.send!('listenFailed');
|
||||
} else {
|
||||
// disableClustering
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(config.port, resolve);
|
||||
});
|
||||
|
|
|
@ -39,28 +39,24 @@ html {
|
|||
width: 28px;
|
||||
height: 28px;
|
||||
transform: translateY(70px);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
#splashSpinner:before,
|
||||
#splashSpinner:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: solid 4px;
|
||||
}
|
||||
|
||||
#splashSpinner:before {
|
||||
border-color: currentColor;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
#splashSpinner:after {
|
||||
#splashSpinner > .spinner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
border-color: currentColor transparent transparent transparent;
|
||||
left: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
fill-rule: evenodd;
|
||||
clip-rule: evenodd;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-miterlimit: 1.5;
|
||||
}
|
||||
#splashSpinner > .spinner.bg {
|
||||
opacity: 0.275;
|
||||
}
|
||||
#splashSpinner > .spinner.fg {
|
||||
animation: splashSpinner 0.5s linear infinite;
|
||||
}
|
||||
|
||||
|
|
|
@ -65,4 +65,14 @@ html
|
|||
div#splash
|
||||
img#splashIcon(src= icon || '/static-assets/splash.png')
|
||||
div#splashSpinner
|
||||
<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1,0,0,1,12,12)">
|
||||
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1,0,0,1,12,12)">
|
||||
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
block content
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as fs from 'node:fs';
|
||||
import * as tmp from 'tmp';
|
||||
import { IImage, convertToJpeg } from './image-processor.js';
|
||||
import * as FFmpeg from 'fluent-ffmpeg';
|
||||
import FFmpeg from 'fluent-ffmpeg';
|
||||
|
||||
export async function GenerateVideoThumbnail(path: string): Promise<IImage> {
|
||||
const [outDir, cleanup] = await new Promise<[string, any]>((res, rej) => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { DOMWindow, JSDOM } from 'jsdom';
|
||||
import fetch from 'node-fetch';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js';
|
||||
import { Instance } from '@/models/entities/instance.js';
|
||||
import { Instances } from '@/models/index.js';
|
||||
|
@ -208,16 +209,11 @@ async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | nul
|
|||
}
|
||||
|
||||
async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
|
||||
if (doc) {
|
||||
const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content');
|
||||
const themeColor = doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') || manifest?.theme_color;
|
||||
|
||||
if (themeColor) {
|
||||
return themeColor;
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest) {
|
||||
return manifest.theme_color;
|
||||
if (themeColor) {
|
||||
const color = new tinycolor(themeColor);
|
||||
if (color.isValid()) return color.toHexString();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -27,6 +27,11 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note,
|
|||
}
|
||||
}
|
||||
|
||||
// check visibility
|
||||
if (!await Notes.isVisibleForMe(note, user)) {
|
||||
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
||||
}
|
||||
|
||||
// TODO: cache
|
||||
reaction = await toDbReaction(reaction, user.host);
|
||||
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import * as childProcess from 'child_process';
|
||||
import * as http from 'node:http';
|
||||
import { SIGKILL } from 'constants';
|
||||
import * as WebSocket from 'ws';
|
||||
import * as misskey from 'misskey-js';
|
||||
import fetch from 'node-fetch';
|
||||
import FormData from 'form-data';
|
||||
import * as childProcess from 'child_process';
|
||||
import * as http from 'node:http';
|
||||
import { DataSource } from 'typeorm';
|
||||
import loadConfig from '../src/config/load.js';
|
||||
import { SIGKILL } from 'constants';
|
||||
import { entities } from '../src/db/postgre.js';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
|
@ -27,29 +28,29 @@ export const async = (fn: Function) => (done: Function) => {
|
|||
|
||||
export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
|
||||
const auth = me ? {
|
||||
i: me.token
|
||||
i: me.token,
|
||||
} : {};
|
||||
|
||||
const res = await fetch(`http://localhost:${port}/api${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(Object.assign(auth, params))
|
||||
body: JSON.stringify(Object.assign(auth, params)),
|
||||
});
|
||||
|
||||
const status = res.status;
|
||||
const body = res.status !== 204 ? await res.json().catch() : null;
|
||||
|
||||
return {
|
||||
body, status
|
||||
body, status,
|
||||
};
|
||||
};
|
||||
|
||||
export const signup = async (params?: any): Promise<any> => {
|
||||
const q = Object.assign({
|
||||
username: 'test',
|
||||
password: 'test'
|
||||
password: 'test',
|
||||
}, params);
|
||||
|
||||
const res = await request('/signup', q);
|
||||
|
@ -59,7 +60,7 @@ export const signup = async (params?: any): Promise<any> => {
|
|||
|
||||
export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
|
||||
const q = Object.assign({
|
||||
text: 'test'
|
||||
text: 'test',
|
||||
}, params);
|
||||
|
||||
const res = await request('/notes/create', q, user);
|
||||
|
@ -70,26 +71,26 @@ export const post = async (user: any, params?: misskey.Endpoints['notes/create']
|
|||
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
|
||||
await request('/notes/reactions/create', {
|
||||
noteId: note.id,
|
||||
reaction: reaction
|
||||
reaction: reaction,
|
||||
}, user);
|
||||
};
|
||||
|
||||
export const uploadFile = (user: any, path?: string): Promise<any> => {
|
||||
const formData = new FormData();
|
||||
formData.append('i', user.token);
|
||||
formData.append('file', fs.createReadStream(path || _dirname + '/resources/Lenna.png'));
|
||||
const formData = new FormData();
|
||||
formData.append('i', user.token);
|
||||
formData.append('file', fs.createReadStream(path || _dirname + '/resources/Lenna.png'));
|
||||
|
||||
return fetch(`http://localhost:${port}/api/drive/files/create`, {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
timeout: 30 * 1000,
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
throw `${res.status} ${res.statusText}`;
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
});
|
||||
return fetch(`http://localhost:${port}/api/drive/files/create`, {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
timeout: 30 * 1000,
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
throw `${res.status} ${res.statusText}`;
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
|
||||
|
@ -112,8 +113,8 @@ export function connectStream(user: any, channel: string, listener: (message: Re
|
|||
channel: channel,
|
||||
id: 'a',
|
||||
pong: true,
|
||||
params: params
|
||||
}
|
||||
params: params,
|
||||
},
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@ -124,8 +125,8 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?
|
|||
return await new Promise((resolve, reject) => {
|
||||
const req = http.request(`http://localhost:${port}${path}`, {
|
||||
headers: {
|
||||
Accept: accept
|
||||
}
|
||||
Accept: accept,
|
||||
},
|
||||
}, res => {
|
||||
if (res.statusCode! >= 400) {
|
||||
reject(res);
|
||||
|
@ -146,7 +147,7 @@ export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProce
|
|||
return (done: (err?: Error) => any) => {
|
||||
const p = childProcess.spawn('node', [_dirname + '/../index.js'], {
|
||||
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
||||
env: { NODE_ENV: 'test', PATH: process.env.PATH }
|
||||
env: { NODE_ENV: 'test', PATH: process.env.PATH },
|
||||
});
|
||||
callbackSpawnedProcess(p);
|
||||
p.on('message', message => {
|
||||
|
@ -158,12 +159,7 @@ export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProce
|
|||
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
||||
if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
|
||||
|
||||
try {
|
||||
const conn = await getConnection();
|
||||
await conn.close();
|
||||
} catch (e) {}
|
||||
|
||||
return await createConnection({
|
||||
return new DataSource({
|
||||
type: 'postgres',
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
|
@ -172,7 +168,7 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) {
|
|||
database: config.db.db,
|
||||
synchronize: true && !justBorrow,
|
||||
dropSchema: true && !justBorrow,
|
||||
entities: initEntities || entities
|
||||
entities: initEntities || entities,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -185,7 +181,7 @@ export function startServer(timeout = 30 * 1000): Promise<childProcess.ChildProc
|
|||
|
||||
const p = childProcess.spawn('node', [_dirname + '/../built/index.js'], {
|
||||
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
|
||||
env: { NODE_ENV: 'test', PATH: process.env.PATH }
|
||||
env: { NODE_ENV: 'test', PATH: process.env.PATH },
|
||||
});
|
||||
|
||||
p.on('error', e => rej(e));
|
||||
|
|
|
@ -705,11 +705,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb"
|
||||
integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==
|
||||
|
||||
"@types/portscanner@2.1.1":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/portscanner/-/portscanner-2.1.1.tgz#89d5094e16f3d941f20f3889dfa5d3a164b3dd3b"
|
||||
integrity sha512-1NsVIbgBKvrqxwtMN0V6CLji1ERwKSI/RWz0J3y++CzSwYNGBStCfpIFgxV3ZwxsDR5PoZqoUWhwraDm+Ztn0Q==
|
||||
|
||||
"@types/pug@2.0.6":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6"
|
||||
|
@ -1249,19 +1244,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
||||
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
|
||||
|
||||
async@>=0.2.9:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
|
||||
|
||||
async@^2.6.0:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
||||
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
async@^3.2.3:
|
||||
async@>=0.2.9, async@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
|
||||
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
|
||||
|
@ -1872,7 +1855,7 @@ color-support@^1.1.2:
|
|||
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
|
||||
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
|
||||
|
||||
color@^4.2.3:
|
||||
color@^4.0.1:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a"
|
||||
integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
|
||||
|
@ -2279,16 +2262,16 @@ detect-file@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
|
||||
integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
|
||||
|
||||
detect-libc@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
|
||||
|
||||
detect-libc@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.0.tgz#c528bc09bc6d1aa30149228240917c225448f204"
|
||||
integrity sha512-S55LzUl8HUav8l9E2PBTlC5PAJrHK7tkM+XXFGD+fbsbkTzhCpG6K05LxJcUOEWzMa4v6ptcMZ9s3fOdJDu0Zw==
|
||||
|
||||
detect-libc@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
|
||||
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
|
||||
|
||||
detect-node@2.1.0, detect-node@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1"
|
||||
|
@ -3861,13 +3844,6 @@ is-negative-zero@^2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
|
||||
integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
|
||||
|
||||
is-number-like@^1.0.3:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/is-number-like/-/is-number-like-1.0.8.tgz#2e129620b50891042e44e9bbbb30593e75cfbbe3"
|
||||
integrity sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==
|
||||
dependencies:
|
||||
lodash.isfinite "^3.3.2"
|
||||
|
||||
is-number-object@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0"
|
||||
|
@ -4498,11 +4474,6 @@ lodash.isequal@^4.5.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
|
||||
|
||||
lodash.isfinite@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
|
||||
integrity sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=
|
||||
|
||||
lodash.isplainobject@^4.0.6:
|
||||
version "4.0.6"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
|
||||
|
@ -4548,7 +4519,7 @@ lodash.union@^4.6.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
|
||||
integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=
|
||||
|
||||
lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21:
|
||||
lodash@^4.17.11, lodash@^4.17.19, lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
@ -4985,7 +4956,7 @@ node-addon-api@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d"
|
||||
integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==
|
||||
|
||||
node-addon-api@^4.3.0:
|
||||
node-addon-api@^4.2.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
|
||||
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
|
||||
|
@ -5541,14 +5512,6 @@ pngjs@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
|
||||
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
|
||||
|
||||
portscanner@2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.2.0.tgz#6059189b3efa0965c9d96a56b958eb9508411cf1"
|
||||
integrity sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==
|
||||
dependencies:
|
||||
async "^2.6.0"
|
||||
is-number-like "^1.0.3"
|
||||
|
||||
postcss@^8.3.11:
|
||||
version "8.3.11"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.11.tgz#c3beca7ea811cd5e1c4a3ec6d2e7599ef1f8f858"
|
||||
|
@ -5580,10 +5543,10 @@ postgres-interval@^1.1.0:
|
|||
dependencies:
|
||||
xtend "^4.0.0"
|
||||
|
||||
prebuild-install@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870"
|
||||
integrity sha512-QBSab31WqkyxpnMWQxubYAHR5S9B2+r81ucocew34Fkl98FhvKIF50jIJnNOBmAZfyNV7vE5T6gd3hTVWgY6tg==
|
||||
prebuild-install@^7.0.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.0.tgz#991b6ac16c81591ba40a6d5de93fb33673ac1370"
|
||||
integrity sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==
|
||||
dependencies:
|
||||
detect-libc "^2.0.0"
|
||||
expand-template "^2.0.3"
|
||||
|
@ -6227,7 +6190,7 @@ seedrandom@3.0.5:
|
|||
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
|
||||
integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
|
||||
|
||||
semver@7.3.7, semver@^7.3.7:
|
||||
semver@7.3.7:
|
||||
version "7.3.7"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f"
|
||||
integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==
|
||||
|
@ -6288,17 +6251,17 @@ sha.js@^2.4.11:
|
|||
inherits "^2.0.1"
|
||||
safe-buffer "^5.0.1"
|
||||
|
||||
sharp@0.30.4:
|
||||
version "0.30.4"
|
||||
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.30.4.tgz#73d9daa63bbc20da189c9328d75d5d395fc8fb73"
|
||||
integrity sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q==
|
||||
sharp@0.29.3:
|
||||
version "0.29.3"
|
||||
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.29.3.tgz#0da183d626094c974516a48fab9b3e4ba92eb5c2"
|
||||
integrity sha512-fKWUuOw77E4nhpyzCCJR1ayrttHoFHBT2U/kR/qEMRhvPEcluG4BKj324+SCO1e84+knXHwhJ1HHJGnUt4ElGA==
|
||||
dependencies:
|
||||
color "^4.2.3"
|
||||
detect-libc "^2.0.1"
|
||||
node-addon-api "^4.3.0"
|
||||
prebuild-install "^7.0.1"
|
||||
semver "^7.3.7"
|
||||
simple-get "^4.0.1"
|
||||
color "^4.0.1"
|
||||
detect-libc "^1.0.3"
|
||||
node-addon-api "^4.2.0"
|
||||
prebuild-install "^7.0.0"
|
||||
semver "^7.3.5"
|
||||
simple-get "^4.0.0"
|
||||
tar-fs "^2.1.1"
|
||||
tunnel-agent "^0.6.0"
|
||||
|
||||
|
@ -6343,7 +6306,7 @@ simple-concat@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
|
||||
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
|
||||
|
||||
simple-get@^4.0.0, simple-get@^4.0.1:
|
||||
simple-get@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
|
||||
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
|
||||
|
|
|
@ -42,7 +42,6 @@
|
|||
"nested-property": "4.0.0",
|
||||
"parse5": "6.0.1",
|
||||
"photoswipe": "5.2.7",
|
||||
"portscanner": "2.2.0",
|
||||
"prismjs": "1.28.0",
|
||||
"private-ip": "2.3.3",
|
||||
"promise-limit": "2.7.0",
|
||||
|
@ -75,6 +74,13 @@
|
|||
"vuedraggable": "4.0.1",
|
||||
"wavesurfer.js": "6.0.1",
|
||||
"websocket": "1.0.34",
|
||||
"@vitejs/plugin-vue": "2.3.3",
|
||||
"@vue/compiler-sfc": "3.2.33",
|
||||
"@rollup/plugin-alias": "3.1.9",
|
||||
"@rollup/plugin-json": "4.1.0",
|
||||
"rollup": "2.73.0",
|
||||
"typescript": "4.6.4",
|
||||
"vite": "2.9.9",
|
||||
"ws": "8.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -99,14 +105,6 @@
|
|||
"@types/ws": "8.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "5.23.0",
|
||||
"@typescript-eslint/parser": "5.23.0",
|
||||
"@vitejs/plugin-vue": "2.3.3",
|
||||
"@vue/compiler-sfc": "3.2.33",
|
||||
"@rollup/plugin-alias": "3.1.9",
|
||||
"@rollup/plugin-json": "4.1.0",
|
||||
"rollup": "2.73.0",
|
||||
"typescript": "4.6.4",
|
||||
|
||||
"vite": "2.9.9",
|
||||
"eslint": "8.15.0",
|
||||
"eslint-plugin-vue": "8.7.1",
|
||||
"cross-env": "7.0.3",
|
||||
|
|
|
@ -1,6 +1,17 @@
|
|||
<template>
|
||||
<div class="yxspomdl" :class="{ inline, colored, mini }">
|
||||
<div class="ring"></div>
|
||||
<div class="container">
|
||||
<svg class="spinner bg" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1.125,0,0,1.125,12,12)">
|
||||
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg class="spinner fg" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
|
||||
<g transform="matrix(1.125,0,0,1.125,12,12)">
|
||||
<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -19,7 +30,7 @@ const props = withDefaults(defineProps<{
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@keyframes ring {
|
||||
@keyframes spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
@ -33,7 +44,7 @@ const props = withDefaults(defineProps<{
|
|||
text-align: center;
|
||||
cursor: wait;
|
||||
|
||||
--size: 48px;
|
||||
--size: 40px;
|
||||
|
||||
&.colored {
|
||||
color: var(--accent);
|
||||
|
@ -50,32 +61,31 @@ const props = withDefaults(defineProps<{
|
|||
--size: 32px;
|
||||
}
|
||||
|
||||
> .ring {
|
||||
> .container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
margin: 0 auto;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
border-radius: 50%;
|
||||
border: solid 4px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
border-color: currentColor;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
&:after {
|
||||
> .spinner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
border-color: currentColor transparent transparent transparent;
|
||||
animation: ring 0.5s linear infinite;
|
||||
left: 0;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
fill-rule: evenodd;
|
||||
clip-rule: evenodd;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-miterlimit: 1.5;
|
||||
}
|
||||
|
||||
> .bg {
|
||||
opacity: 0.275;
|
||||
}
|
||||
|
||||
> .fg {
|
||||
animation: spinner 0.5s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,32 @@ const props = withDefaults(defineProps<{
|
|||
}
|
||||
}
|
||||
|
||||
.mfm-x2 {
|
||||
--mfm-zoom-size: 200%;
|
||||
}
|
||||
|
||||
.mfm-x3 {
|
||||
--mfm-zoom-size: 400%;
|
||||
}
|
||||
|
||||
.mfm-x4 {
|
||||
--mfm-zoom-size: 600%;
|
||||
}
|
||||
|
||||
.mfm-x2, .mfm-x3, .mfm-x4 {
|
||||
font-size: var(--mfm-zoom-size);
|
||||
|
||||
.mfm-x2, .mfm-x3, .mfm-x4 {
|
||||
/* only half effective */
|
||||
font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
|
||||
|
||||
.mfm-x2, .mfm-x3, .mfm-x4 {
|
||||
/* disabled */
|
||||
font-size: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes mfm-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
|
|
|
@ -39,6 +39,19 @@ const bg = {
|
|||
border-radius: 4px 0 0 4px;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
text-shadow: /* .866 ≈ sin(60deg) */
|
||||
1px 0 1px #000,
|
||||
.866px .5px 1px #000,
|
||||
.5px .866px 1px #000,
|
||||
0 1px 1px #000,
|
||||
-.5px .866px 1px #000,
|
||||
-.866px .5px 1px #000,
|
||||
-1px 0 1px #000,
|
||||
-.866px -.5px 1px #000,
|
||||
-.5px -.866px 1px #000,
|
||||
0 -1px 1px #000,
|
||||
.5px -.866px 1px #000,
|
||||
.866px -.5px 1px #000;
|
||||
|
||||
> .icon {
|
||||
height: 100%;
|
||||
|
|
|
@ -142,16 +142,19 @@ export default defineComponent({
|
|||
break;
|
||||
}
|
||||
case 'x2': {
|
||||
style = `font-size: 200%;`;
|
||||
break;
|
||||
return h('span', {
|
||||
class: 'mfm-x2',
|
||||
}, genEl(token.children));
|
||||
}
|
||||
case 'x3': {
|
||||
style = `font-size: 400%;`;
|
||||
break;
|
||||
return h('span', {
|
||||
class: 'mfm-x3',
|
||||
}, genEl(token.children));
|
||||
}
|
||||
case 'x4': {
|
||||
style = `font-size: 600%;`;
|
||||
break;
|
||||
return h('span', {
|
||||
class: 'mfm-x4',
|
||||
}, genEl(token.children));
|
||||
}
|
||||
case 'font': {
|
||||
const family =
|
||||
|
|
|
@ -1,34 +1,22 @@
|
|||
<template>
|
||||
<div class="lzyxtsnt">
|
||||
<img v-if="image" :src="image.url"/>
|
||||
<ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
|
||||
import * as os from '@/os';
|
||||
import { ImageBlock } from '@/scripts/hpml/block';
|
||||
import { Hpml } from '@/scripts/hpml/evaluator';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
block: {
|
||||
type: Object as PropType<ImageBlock>,
|
||||
required: true
|
||||
},
|
||||
hpml: {
|
||||
type: Object as PropType<Hpml>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props, ctx) {
|
||||
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
|
||||
const props = defineProps<{
|
||||
block: PropType<ImageBlock>,
|
||||
hpml: PropType<Hpml>,
|
||||
}>();
|
||||
|
||||
return {
|
||||
image
|
||||
};
|
||||
}
|
||||
});
|
||||
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
||||
@click="toggleReaction()"
|
||||
>
|
||||
<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
|
||||
<span>{{ count }}</span>
|
||||
<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/>
|
||||
<span class="count">{{ count }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
@ -141,12 +141,16 @@ export default defineComponent({
|
|||
background: var(--accent);
|
||||
}
|
||||
|
||||
> span {
|
||||
> .count {
|
||||
color: var(--fgOnAccent);
|
||||
}
|
||||
|
||||
> .icon {
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
> .count {
|
||||
font-size: 0.9em;
|
||||
line-height: 32px;
|
||||
margin: 0 0 0 4px;
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
<XModalWindow ref="dialog"
|
||||
:width="370"
|
||||
:height="400"
|
||||
@close="dialog.close()"
|
||||
@close="onClose"
|
||||
@closed="emit('closed')"
|
||||
>
|
||||
<template #header>{{ $ts.login }}</template>
|
||||
|
||||
<MkSignin :auto-set="autoSet" @login="onLogin"/>
|
||||
<MkSignin :auto-set="autoSet" :message="message" @login="onLogin"/>
|
||||
</XModalWindow>
|
||||
</template>
|
||||
|
||||
|
@ -18,17 +18,25 @@ import MkSignin from './signin.vue';
|
|||
|
||||
const props = withDefaults(defineProps<{
|
||||
autoSet?: boolean;
|
||||
message?: string,
|
||||
}>(), {
|
||||
autoSet: false,
|
||||
message: ''
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'closed'): void;
|
||||
(ev: 'done'): void;
|
||||
(ev: 'closed'): void;
|
||||
(ev: 'cancelled'): void;
|
||||
}>();
|
||||
|
||||
const dialog = $ref<InstanceType<typeof XModalWindow>>();
|
||||
|
||||
function onClose() {
|
||||
emit('cancelled');
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
function onLogin(res) {
|
||||
emit('done', res);
|
||||
dialog.close();
|
||||
|
|
|
@ -1,39 +1,44 @@
|
|||
<template>
|
||||
<form class="eppvobhk _monolithic_" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
||||
<div class="auth _section _formRoot">
|
||||
<div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }"></div>
|
||||
<div v-show="withAvatar" class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null, marginBottom: message ? '1.5em' : null }"></div>
|
||||
<MkInfo v-if="message">
|
||||
{{ message }}
|
||||
</MkInfo>
|
||||
<div v-if="!totpLogin" class="normal-signin">
|
||||
<MkInput v-model="username" class="_formBlock" :placeholder="$ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||
<MkInput v-model="username" class="_formBlock" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="$ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
|
||||
<MkInput v-if="!user || user && !user.usePasswordLessLogin" v-model="password" class="_formBlock" :placeholder="i18n.ts.password" type="password" :with-password-toggle="true" required data-cy-signin-password>
|
||||
<template #prefix><i class="fas fa-lock"></i></template>
|
||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ $ts.forgotPassword }}</button></template>
|
||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
||||
</MkInput>
|
||||
<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
|
||||
<MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
|
||||
<MkButton class="_formBlock" type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
</div>
|
||||
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
|
||||
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
||||
<p>{{ $ts.tapSecurityKey }}</p>
|
||||
<p>{{ i18n.ts.tapSecurityKey }}</p>
|
||||
<MkButton v-if="!queryingKey" @click="queryKey">
|
||||
{{ $ts.retry }}
|
||||
{{ i18n.ts.retry }}
|
||||
</MkButton>
|
||||
</div>
|
||||
<div v-if="user && user.securityKeys" class="or-hr">
|
||||
<p class="or-msg">{{ $ts.or }}</p>
|
||||
<p class="or-msg">{{ i18n.ts.or }}</p>
|
||||
</div>
|
||||
<div class="twofa-group totp-group">
|
||||
<p style="margin-bottom:0;">{{ $ts.twoStepAuthentication }}</p>
|
||||
<p style="margin-bottom:0;">{{ i18n.ts.twoStepAuthentication }}</p>
|
||||
<MkInput v-if="user && user.usePasswordLessLogin" v-model="password" type="password" :with-password-toggle="true" required>
|
||||
<template #label>{{ $ts.password }}</template>
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="fas fa-lock"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
|
||||
<template #label>{{ $ts.token }}</template>
|
||||
<template #label>{{ i18n.ts.token }}</template>
|
||||
<template #prefix><i class="fas fa-gavel"></i></template>
|
||||
</MkInput>
|
||||
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton>
|
||||
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -45,190 +50,192 @@
|
|||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineAsyncComponent, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import { apiUrl, host } from '@/config';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
import { apiUrl, host as configHost } from '@/config';
|
||||
import { byteify, hexify } from '@/scripts/2fa';
|
||||
import * as os from '@/os';
|
||||
import { login } from '@/account';
|
||||
import { showSuspendedDialog } from '../scripts/show-suspended-dialog';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkInput,
|
||||
const MkCaptcha = defineAsyncComponent(() => import('./captcha.vue'));
|
||||
|
||||
let signing = $ref(false);
|
||||
let user = $ref(null);
|
||||
let username = $ref('');
|
||||
let password = $ref('');
|
||||
let token = $ref('');
|
||||
let host = $ref(toUnicode(configHost));
|
||||
let totpLogin = $ref(false);
|
||||
let credential = $ref(null);
|
||||
let challengeData = $ref(null);
|
||||
let queryingKey = $ref(false);
|
||||
let hCaptchaResponse = $ref(null);
|
||||
let reCaptchaResponse = $ref(null);
|
||||
|
||||
const meta = $computed(() => instance);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'login', v: any): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps({
|
||||
withAvatar: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
|
||||
props: {
|
||||
withAvatar: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
autoSet: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
autoSet: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
emits: ['login'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
signing: false,
|
||||
user: null,
|
||||
username: '',
|
||||
password: '',
|
||||
token: '',
|
||||
apiUrl,
|
||||
host: toUnicode(host),
|
||||
totpLogin: false,
|
||||
credential: null,
|
||||
challengeData: null,
|
||||
queryingKey: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
meta() {
|
||||
return this.$instance;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
onUsernameChange() {
|
||||
os.api('users/show', {
|
||||
username: this.username
|
||||
}).then(user => {
|
||||
this.user = user;
|
||||
}, () => {
|
||||
this.user = null;
|
||||
});
|
||||
},
|
||||
|
||||
onLogin(res) {
|
||||
if (this.autoSet) {
|
||||
return login(res.i);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
queryKey() {
|
||||
this.queryingKey = true;
|
||||
return navigator.credentials.get({
|
||||
publicKey: {
|
||||
challenge: byteify(this.challengeData.challenge, 'base64'),
|
||||
allowCredentials: this.challengeData.securityKeys.map(key => ({
|
||||
id: byteify(key.id, 'hex'),
|
||||
type: 'public-key',
|
||||
transports: ['usb', 'nfc', 'ble', 'internal']
|
||||
})),
|
||||
timeout: 60 * 1000
|
||||
}
|
||||
}).catch(() => {
|
||||
this.queryingKey = false;
|
||||
return Promise.reject(null);
|
||||
}).then(credential => {
|
||||
this.queryingKey = false;
|
||||
this.signing = true;
|
||||
return os.api('signin', {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
signature: hexify(credential.response.signature),
|
||||
authenticatorData: hexify(credential.response.authenticatorData),
|
||||
clientDataJSON: hexify(credential.response.clientDataJSON),
|
||||
credentialId: credential.id,
|
||||
challengeId: this.challengeData.challengeId
|
||||
});
|
||||
}).then(res => {
|
||||
this.$emit('login', res);
|
||||
return this.onLogin(res);
|
||||
}).catch(err => {
|
||||
if (err === null) return;
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: this.$ts.signinFailed
|
||||
});
|
||||
this.signing = false;
|
||||
});
|
||||
},
|
||||
|
||||
onSubmit() {
|
||||
this.signing = true;
|
||||
if (!this.totpLogin && this.user && this.user.twoFactorEnabled) {
|
||||
if (window.PublicKeyCredential && this.user.securityKeys) {
|
||||
os.api('signin', {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
}).then(res => {
|
||||
this.totpLogin = true;
|
||||
this.signing = false;
|
||||
this.challengeData = res;
|
||||
return this.queryKey();
|
||||
}).catch(this.loginFailed);
|
||||
} else {
|
||||
this.totpLogin = true;
|
||||
this.signing = false;
|
||||
}
|
||||
} else {
|
||||
os.api('signin', {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
token: this.user && this.user.twoFactorEnabled ? this.token : undefined
|
||||
}).then(res => {
|
||||
this.$emit('login', res);
|
||||
this.onLogin(res);
|
||||
}).catch(this.loginFailed);
|
||||
}
|
||||
},
|
||||
|
||||
loginFailed(err) {
|
||||
switch (err.id) {
|
||||
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: this.$ts.loginFailed,
|
||||
text: this.$ts.noSuchUser
|
||||
});
|
||||
break;
|
||||
}
|
||||
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: this.$ts.loginFailed,
|
||||
text: this.$ts.incorrectPassword,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
|
||||
showSuspendedDialog();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: this.$ts.loginFailed,
|
||||
text: JSON.stringify(err)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.challengeData = null;
|
||||
this.totpLogin = false;
|
||||
this.signing = false;
|
||||
},
|
||||
|
||||
resetPassword() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {
|
||||
}, 'closed');
|
||||
}
|
||||
message: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
function onUsernameChange() {
|
||||
os.api('users/show', {
|
||||
username: username
|
||||
}).then(user => {
|
||||
user = user;
|
||||
}, () => {
|
||||
user = null;
|
||||
});
|
||||
}
|
||||
|
||||
function onLogin(res) {
|
||||
if (props.autoSet) {
|
||||
return login(res.i);
|
||||
}
|
||||
}
|
||||
|
||||
function queryKey() {
|
||||
queryingKey = true;
|
||||
return navigator.credentials.get({
|
||||
publicKey: {
|
||||
challenge: byteify(challengeData.challenge, 'base64'),
|
||||
allowCredentials: challengeData.securityKeys.map(key => ({
|
||||
id: byteify(key.id, 'hex'),
|
||||
type: 'public-key',
|
||||
transports: ['usb', 'nfc', 'ble', 'internal']
|
||||
})),
|
||||
timeout: 60 * 1000
|
||||
}
|
||||
}).catch(() => {
|
||||
queryingKey = false;
|
||||
return Promise.reject(null);
|
||||
}).then(credential => {
|
||||
queryingKey = false;
|
||||
signing = true;
|
||||
return os.api('signin', {
|
||||
username,
|
||||
password,
|
||||
signature: hexify(credential.response.signature),
|
||||
authenticatorData: hexify(credential.response.authenticatorData),
|
||||
clientDataJSON: hexify(credential.response.clientDataJSON),
|
||||
credentialId: credential.id,
|
||||
challengeId: challengeData.challengeId,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'g-recaptcha-response': reCaptchaResponse,
|
||||
});
|
||||
}).then(res => {
|
||||
emit('login', res);
|
||||
return onLogin(res);
|
||||
}).catch(err => {
|
||||
if (err === null) return;
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.signinFailed
|
||||
});
|
||||
signing = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
signing = true;
|
||||
console.log('submit')
|
||||
if (!totpLogin && user && user.twoFactorEnabled) {
|
||||
if (window.PublicKeyCredential && user.securityKeys) {
|
||||
os.api('signin', {
|
||||
username,
|
||||
password,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'g-recaptcha-response': reCaptchaResponse,
|
||||
}).then(res => {
|
||||
totpLogin = true;
|
||||
signing = false;
|
||||
challengeData = res;
|
||||
return queryKey();
|
||||
}).catch(loginFailed);
|
||||
} else {
|
||||
totpLogin = true;
|
||||
signing = false;
|
||||
}
|
||||
} else {
|
||||
os.api('signin', {
|
||||
username,
|
||||
password,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'g-recaptcha-response': reCaptchaResponse,
|
||||
token: user && user.twoFactorEnabled ? token : undefined
|
||||
}).then(res => {
|
||||
emit('login', res);
|
||||
onLogin(res);
|
||||
}).catch(loginFailed);
|
||||
}
|
||||
}
|
||||
|
||||
function loginFailed(err) {
|
||||
switch (err.id) {
|
||||
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.loginFailed,
|
||||
text: i18n.ts.noSuchUser
|
||||
});
|
||||
break;
|
||||
}
|
||||
case '932c904e-9460-45b7-9ce6-7ed33be7eb2c': {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.loginFailed,
|
||||
text: i18n.ts.incorrectPassword,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'e03a5f46-d309-4865-9b69-56282d94e1eb': {
|
||||
showSuspendedDialog();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.log(err)
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.loginFailed,
|
||||
text: JSON.stringify(err)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
challengeData = null;
|
||||
totpLogin = false;
|
||||
signing = false;
|
||||
}
|
||||
|
||||
function resetPassword() {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/forgot-password.vue')), {}, {
|
||||
}, 'closed');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -58,8 +58,8 @@
|
|||
</template>
|
||||
</I18n>
|
||||
</MkSwitch>
|
||||
<captcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
|
||||
<captcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/>
|
||||
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton>
|
||||
</template>
|
||||
</form>
|
||||
|
@ -81,7 +81,7 @@ export default defineComponent({
|
|||
MkButton,
|
||||
MkInput,
|
||||
MkSwitch,
|
||||
captcha: defineAsyncComponent(() => import('./captcha.vue')),
|
||||
MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')),
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
|
@ -14,8 +14,14 @@
|
|||
</div>
|
||||
|
||||
<div v-else ref="rootEl">
|
||||
<div v-show="pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
|
||||
<MkButton v-if="!moreFetching" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMoreAhead">
|
||||
{{ $ts.loadMore }}
|
||||
</MkButton>
|
||||
<MkLoading v-else class="loading"/>
|
||||
</div>
|
||||
<slot :items="items"></slot>
|
||||
<div v-show="more" key="_more_" class="cxiknjgy _gap">
|
||||
<div v-show="!pagination.reversed && more" key="_more_" class="cxiknjgy _gap">
|
||||
<MkButton v-if="!moreFetching" v-appear="($store.state.enableInfiniteScroll && !disableAutoLoad) ? fetchMore : null" class="button" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }" primary @click="fetchMore">
|
||||
{{ $ts.loadMore }}
|
||||
</MkButton>
|
||||
|
@ -244,6 +250,11 @@ const append = (item: Item): void => {
|
|||
items.value.push(item);
|
||||
};
|
||||
|
||||
const removeItem = (finder: (item: Item) => boolean) => {
|
||||
const i = items.value.findIndex(finder);
|
||||
items.value.splice(i, 1);
|
||||
};
|
||||
|
||||
const updateItem = (id: Item['id'], replacer: (old: Item) => Item): void => {
|
||||
const i = items.value.findIndex(item => item.id === id);
|
||||
items.value[i] = replacer(items.value[i]);
|
||||
|
@ -273,9 +284,9 @@ defineExpose({
|
|||
queue,
|
||||
backed,
|
||||
reload,
|
||||
fetchMoreAhead,
|
||||
prepend,
|
||||
append,
|
||||
removeItem,
|
||||
updateItem,
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -150,6 +150,7 @@ const patrons = [
|
|||
'Weeble',
|
||||
'蝉暮せせせ',
|
||||
'ThatOneCalculator',
|
||||
'pixeldesu',
|
||||
];
|
||||
|
||||
let easterEggReady = false;
|
||||
|
|
|
@ -27,85 +27,71 @@
|
|||
</XModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkInput from '@/components/form/input.vue';
|
||||
import * as os from '@/os';
|
||||
import { unique } from '@/scripts/array';
|
||||
import { i18n } from '@/i18n';
|
||||
import { emojiCategories } from '@/instance';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
XModalWindow,
|
||||
MkButton,
|
||||
MkInput,
|
||||
},
|
||||
const props = defineProps<{
|
||||
emoji: any,
|
||||
}>();
|
||||
|
||||
props: {
|
||||
emoji: {
|
||||
required: true,
|
||||
let dialog = $ref(null);
|
||||
let name: string = $ref(props.emoji.name);
|
||||
let category: string = $ref(props.emoji.category);
|
||||
let aliases: string = $ref(props.emoji.aliases.join(' '));
|
||||
let categories: string[] = $ref(emojiCategories);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'done', v: { deleted?: boolean, updated?: any }): void,
|
||||
(ev: 'closed'): void
|
||||
}>();
|
||||
|
||||
function ok() {
|
||||
update();
|
||||
}
|
||||
|
||||
async function update() {
|
||||
await os.apiWithDialog('admin/emoji/update', {
|
||||
id: props.emoji.id,
|
||||
name,
|
||||
category,
|
||||
aliases: aliases.split(' '),
|
||||
});
|
||||
|
||||
emit('done', {
|
||||
updated: {
|
||||
id: props.emoji.id,
|
||||
name,
|
||||
category,
|
||||
aliases: aliases.split(' '),
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
emits: ['done', 'closed'],
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
name: this.emoji.name,
|
||||
category: this.emoji.category,
|
||||
aliases: this.emoji.aliases?.join(' '),
|
||||
categories: [],
|
||||
}
|
||||
},
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
created() {
|
||||
os.api('meta', { detail: false }).then(({ emojis }) => {
|
||||
this.categories = unique(emojis.map((x: any) => x.category || '').filter((x: string) => x !== ''));
|
||||
os.api('admin/emoji/delete', {
|
||||
id: props.emoji.id
|
||||
}).then(() => {
|
||||
emit('done', {
|
||||
deleted: true
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
ok() {
|
||||
this.update();
|
||||
},
|
||||
|
||||
async update() {
|
||||
await os.apiWithDialog('admin/emoji/update', {
|
||||
id: this.emoji.id,
|
||||
name: this.name,
|
||||
category: this.category,
|
||||
aliases: this.aliases.split(' '),
|
||||
});
|
||||
|
||||
this.$emit('done', {
|
||||
updated: {
|
||||
name: this.name,
|
||||
category: this.category,
|
||||
aliases: this.aliases.split(' '),
|
||||
}
|
||||
});
|
||||
this.$refs.dialog.close();
|
||||
},
|
||||
|
||||
async del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$t('removeAreYouSure', { x: this.emoji.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
os.api('admin/emoji/delete', {
|
||||
id: this.emoji.id
|
||||
}).then(() => {
|
||||
this.$emit('done', {
|
||||
deleted: true
|
||||
});
|
||||
this.$refs.dialog.close();
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
dialog.close();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -135,12 +135,12 @@ const edit = (emoji) => {
|
|||
}, {
|
||||
done: result => {
|
||||
if (result.updated) {
|
||||
emojisPaginationComponent.value.replaceItem(item => item.id === emoji.id, {
|
||||
...emoji,
|
||||
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({
|
||||
...oldEmoji,
|
||||
...result.updated
|
||||
});
|
||||
}));
|
||||
} else if (result.deleted) {
|
||||
emojisPaginationComponent.value.removeItem(item => item.id === emoji.id);
|
||||
emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id);
|
||||
}
|
||||
},
|
||||
}, 'closed');
|
||||
|
|
|
@ -34,74 +34,52 @@
|
|||
</XModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import MkSwitch from '@/components/form/switch.vue';
|
||||
import XModalWindow from '@/components/ui/modal-window.vue';
|
||||
import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue';
|
||||
import bytes from '@/filters/bytes';
|
||||
import * as os from '@/os';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
MkSwitch,
|
||||
XModalWindow,
|
||||
MkDriveFileThumbnail,
|
||||
},
|
||||
let file: any = $ref(null);
|
||||
let info: any = $ref(null);
|
||||
let isSensitive: boolean = $ref(false);
|
||||
|
||||
props: {
|
||||
fileId: {
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
const props = defineProps<{
|
||||
fileId: string,
|
||||
}>();
|
||||
|
||||
emits: ['closed'],
|
||||
async function fetch() {
|
||||
file = await os.api('drive/files/show', { fileId: props.fileId });
|
||||
info = await os.api('admin/drive/show-file', { fileId: props.fileId });
|
||||
isSensitive = file.isSensitive;
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
file: null,
|
||||
info: null,
|
||||
isSensitive: false,
|
||||
};
|
||||
},
|
||||
fetch();
|
||||
|
||||
created() {
|
||||
this.fetch();
|
||||
},
|
||||
function showUser() {
|
||||
os.pageWindow(`/user-info/${file.userId}`);
|
||||
}
|
||||
|
||||
methods: {
|
||||
async fetch() {
|
||||
this.file = await os.api('drive/files/show', { fileId: this.fileId });
|
||||
this.info = await os.api('admin/drive/show-file', { fileId: this.fileId });
|
||||
this.isSensitive = this.file.isSensitive;
|
||||
},
|
||||
async function del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: file.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
showUser() {
|
||||
os.pageWindow(`/user-info/${this.file.userId}`);
|
||||
},
|
||||
os.apiWithDialog('drive/files/delete', {
|
||||
fileId: file.id
|
||||
});
|
||||
}
|
||||
|
||||
async del() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: this.$t('removeAreYouSure', { x: this.file.name }),
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('drive/files/delete', {
|
||||
fileId: this.file.id
|
||||
});
|
||||
},
|
||||
|
||||
async toggleIsSensitive(v) {
|
||||
await os.api('drive/files/update', { fileId: this.fileId, isSensitive: v });
|
||||
this.isSensitive = v;
|
||||
},
|
||||
|
||||
bytes
|
||||
}
|
||||
});
|
||||
async function toggleIsSensitive(v) {
|
||||
await os.api('drive/files/update', { fileId: props.fileId, isSensitive: v });
|
||||
isSensitive = v;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div ref="el" class="hiyeyicy" :class="{ wide: !narrow }">
|
||||
<div v-if="!narrow || page == null" class="nav">
|
||||
<div v-if="!narrow || initialPage == null" class="nav">
|
||||
<MkHeader :info="header"></MkHeader>
|
||||
|
||||
<MkSpacer :content-max="700" :margin-min="16">
|
||||
|
@ -12,21 +12,21 @@
|
|||
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ $ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ $ts.configure }}</MkA></MkInfo>
|
||||
<MkInfo v-if="noBotProtection" warn class="info">{{ $ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ $ts.configure }}</MkA></MkInfo>
|
||||
|
||||
<MkSuperMenu :def="menuDef" :grid="page == null"></MkSuperMenu>
|
||||
<MkSuperMenu :def="menuDef" :grid="initialPage == null"></MkSuperMenu>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div v-if="!(narrow && initialPage == null)" class="main">
|
||||
<MkStickyContainer>
|
||||
<template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template>
|
||||
<component :is="component" :ref="el => pageChanged(el)" :key="page" v-bind="pageProps"/>
|
||||
<component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineAsyncComponent, defineComponent, isRef, nextTick, onMounted, reactive, ref, watch } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, nextTick, onMounted, onUnmounted, provide, watch } from 'vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkSuperMenu from '@/components/ui/super-menu.vue';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
|
@ -35,292 +35,277 @@ import { instance } from '@/instance';
|
|||
import * as symbols from '@/symbols';
|
||||
import * as os from '@/os';
|
||||
import { lookupUser } from '@/scripts/lookup-user';
|
||||
import { MisskeyNavigator } from '@/scripts/navigate';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkSuperMenu,
|
||||
MkInfo,
|
||||
},
|
||||
const isEmpty = (x: string | null) => x == null || x === '';
|
||||
|
||||
provide: {
|
||||
shouldOmitHeaderTitle: false,
|
||||
},
|
||||
const nav = new MisskeyNavigator();
|
||||
|
||||
props: {
|
||||
initialPage: {
|
||||
type: String,
|
||||
required: false
|
||||
const indexInfo = {
|
||||
title: i18n.ts.controlPanel,
|
||||
icon: 'fas fa-cog',
|
||||
bg: 'var(--bg)',
|
||||
hideHeader: true,
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
initialPage?: string,
|
||||
}>();
|
||||
|
||||
provide('shouldOmitHeaderTitle', false);
|
||||
|
||||
let INFO = $ref(indexInfo);
|
||||
let childInfo = $ref(null);
|
||||
let page = $ref(props.initialPage);
|
||||
let narrow = $ref(false);
|
||||
let view = $ref(null);
|
||||
let el = $ref(null);
|
||||
let pageProps = $ref({});
|
||||
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
|
||||
let noBotProtection = !instance.enableHcaptcha && !instance.enableRecaptcha;
|
||||
|
||||
const NARROW_THRESHOLD = 600;
|
||||
const ro = new ResizeObserver((entries, observer) => {
|
||||
if (entries.length === 0) return;
|
||||
narrow = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
|
||||
});
|
||||
|
||||
const menuDef = $computed(() => [{
|
||||
title: i18n.ts.quickAction,
|
||||
items: [{
|
||||
type: 'button',
|
||||
icon: 'fas fa-search',
|
||||
text: i18n.ts.lookup,
|
||||
action: lookup,
|
||||
}, ...(instance.disableRegistration ? [{
|
||||
type: 'button',
|
||||
icon: 'fas fa-user',
|
||||
text: i18n.ts.invite,
|
||||
action: invite,
|
||||
}] : [])],
|
||||
}, {
|
||||
title: i18n.ts.administration,
|
||||
items: [{
|
||||
icon: 'fas fa-tachometer-alt',
|
||||
text: i18n.ts.dashboard,
|
||||
to: '/admin/overview',
|
||||
active: props.initialPage === 'overview',
|
||||
}, {
|
||||
icon: 'fas fa-users',
|
||||
text: i18n.ts.users,
|
||||
to: '/admin/users',
|
||||
active: props.initialPage === 'users',
|
||||
}, {
|
||||
icon: 'fas fa-laugh',
|
||||
text: i18n.ts.customEmojis,
|
||||
to: '/admin/emojis',
|
||||
active: props.initialPage === 'emojis',
|
||||
}, {
|
||||
icon: 'fas fa-globe',
|
||||
text: i18n.ts.federation,
|
||||
to: '/admin/federation',
|
||||
active: props.initialPage === 'federation',
|
||||
}, {
|
||||
icon: 'fas fa-clipboard-list',
|
||||
text: i18n.ts.jobQueue,
|
||||
to: '/admin/queue',
|
||||
active: props.initialPage === 'queue',
|
||||
}, {
|
||||
icon: 'fas fa-cloud',
|
||||
text: i18n.ts.files,
|
||||
to: '/admin/files',
|
||||
active: props.initialPage === 'files',
|
||||
}, {
|
||||
icon: 'fas fa-broadcast-tower',
|
||||
text: i18n.ts.announcements,
|
||||
to: '/admin/announcements',
|
||||
active: props.initialPage === 'announcements',
|
||||
}, {
|
||||
icon: 'fas fa-audio-description',
|
||||
text: i18n.ts.ads,
|
||||
to: '/admin/ads',
|
||||
active: props.initialPage === 'ads',
|
||||
}, {
|
||||
icon: 'fas fa-exclamation-circle',
|
||||
text: i18n.ts.abuseReports,
|
||||
to: '/admin/abuses',
|
||||
active: props.initialPage === 'abuses',
|
||||
}],
|
||||
}, {
|
||||
title: i18n.ts.settings,
|
||||
items: [{
|
||||
icon: 'fas fa-cog',
|
||||
text: i18n.ts.general,
|
||||
to: '/admin/settings',
|
||||
active: props.initialPage === 'settings',
|
||||
}, {
|
||||
icon: 'fas fa-envelope',
|
||||
text: i18n.ts.emailServer,
|
||||
to: '/admin/email-settings',
|
||||
active: props.initialPage === 'email-settings',
|
||||
}, {
|
||||
icon: 'fas fa-cloud',
|
||||
text: i18n.ts.objectStorage,
|
||||
to: '/admin/object-storage',
|
||||
active: props.initialPage === 'object-storage',
|
||||
}, {
|
||||
icon: 'fas fa-lock',
|
||||
text: i18n.ts.security,
|
||||
to: '/admin/security',
|
||||
active: props.initialPage === 'security',
|
||||
}, {
|
||||
icon: 'fas fa-globe',
|
||||
text: i18n.ts.relays,
|
||||
to: '/admin/relays',
|
||||
active: props.initialPage === 'relays',
|
||||
}, {
|
||||
icon: 'fas fa-share-alt',
|
||||
text: i18n.ts.integration,
|
||||
to: '/admin/integrations',
|
||||
active: props.initialPage === 'integrations',
|
||||
}, {
|
||||
icon: 'fas fa-ban',
|
||||
text: i18n.ts.instanceBlocking,
|
||||
to: '/admin/instance-block',
|
||||
active: props.initialPage === 'instance-block',
|
||||
}, {
|
||||
icon: 'fas fa-ghost',
|
||||
text: i18n.ts.proxyAccount,
|
||||
to: '/admin/proxy-account',
|
||||
active: props.initialPage === 'proxy-account',
|
||||
}, {
|
||||
icon: 'fas fa-cogs',
|
||||
text: i18n.ts.other,
|
||||
to: '/admin/other-settings',
|
||||
active: props.initialPage === 'other-settings',
|
||||
}],
|
||||
}, {
|
||||
title: i18n.ts.info,
|
||||
items: [{
|
||||
icon: 'fas fa-database',
|
||||
text: i18n.ts.database,
|
||||
to: '/admin/database',
|
||||
active: props.initialPage === 'database',
|
||||
}],
|
||||
}]);
|
||||
|
||||
const component = $computed(() => {
|
||||
if (props.initialPage == null) return null;
|
||||
switch (props.initialPage) {
|
||||
case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
|
||||
case 'users': return defineAsyncComponent(() => import('./users.vue'));
|
||||
case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
|
||||
case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
|
||||
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
|
||||
case 'files': return defineAsyncComponent(() => import('./files.vue'));
|
||||
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
|
||||
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
|
||||
case 'database': return defineAsyncComponent(() => import('./database.vue'));
|
||||
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
|
||||
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
|
||||
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
|
||||
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
|
||||
case 'security': return defineAsyncComponent(() => import('./security.vue'));
|
||||
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
|
||||
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
|
||||
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
|
||||
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
|
||||
case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
|
||||
}
|
||||
});
|
||||
|
||||
watch(component, () => {
|
||||
pageProps = {};
|
||||
|
||||
nextTick(() => {
|
||||
scroll(el, { top: 0 });
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => props.initialPage, () => {
|
||||
if (props.initialPage == null && !narrow) {
|
||||
nav.push('/admin/overview');
|
||||
} else {
|
||||
if (props.initialPage == null) {
|
||||
INFO = indexInfo;
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
setup(props, context) {
|
||||
const indexInfo = {
|
||||
title: i18n.ts.controlPanel,
|
||||
icon: 'fas fa-cog',
|
||||
bg: 'var(--bg)',
|
||||
hideHeader: true,
|
||||
};
|
||||
const INFO = ref(indexInfo);
|
||||
const childInfo = ref(null);
|
||||
const page = ref(props.initialPage);
|
||||
const narrow = ref(false);
|
||||
const view = ref(null);
|
||||
const el = ref(null);
|
||||
const pageChanged = (page) => {
|
||||
if (page == null) return;
|
||||
const viewInfo = page[symbols.PAGE_INFO];
|
||||
if (isRef(viewInfo)) {
|
||||
watch(viewInfo, () => {
|
||||
childInfo.value = viewInfo.value;
|
||||
}, { immediate: true });
|
||||
} else {
|
||||
childInfo.value = viewInfo;
|
||||
}
|
||||
};
|
||||
const pageProps = ref({});
|
||||
watch(narrow, () => {
|
||||
if (props.initialPage == null && !narrow) {
|
||||
nav.push('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
||||
const isEmpty = (x: any) => x == null || x == '';
|
||||
onMounted(() => {
|
||||
ro.observe(el);
|
||||
|
||||
const noMaintainerInformation = ref(false);
|
||||
const noBotProtection = ref(false);
|
||||
narrow = el.offsetWidth < NARROW_THRESHOLD;
|
||||
if (props.initialPage == null && !narrow) {
|
||||
nav.push('/admin/overview');
|
||||
}
|
||||
});
|
||||
|
||||
os.api('meta', { detail: true }).then(meta => {
|
||||
// TODO: 設定が完了しても残ったままになるので、ストリーミングでmeta更新イベントを受け取ってよしなに更新する
|
||||
noMaintainerInformation.value = isEmpty(meta.maintainerName) || isEmpty(meta.maintainerEmail);
|
||||
noBotProtection.value = !meta.enableHcaptcha && !meta.enableRecaptcha;
|
||||
onUnmounted(() => {
|
||||
ro.disconnect();
|
||||
});
|
||||
|
||||
const pageChanged = (page) => {
|
||||
if (page == null) {
|
||||
childInfo = null;
|
||||
} else {
|
||||
childInfo = page[symbols.PAGE_INFO];
|
||||
}
|
||||
};
|
||||
|
||||
const invite = () => {
|
||||
os.api('admin/invite').then(x => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: x.code
|
||||
});
|
||||
|
||||
const menuDef = computed(() => [{
|
||||
title: i18n.ts.quickAction,
|
||||
items: [{
|
||||
type: 'button',
|
||||
icon: 'fas fa-search',
|
||||
text: i18n.ts.lookup,
|
||||
action: lookup,
|
||||
}, ...(instance.disableRegistration ? [{
|
||||
type: 'button',
|
||||
icon: 'fas fa-user',
|
||||
text: i18n.ts.invite,
|
||||
action: invite,
|
||||
}] : [])],
|
||||
}, {
|
||||
title: i18n.ts.administration,
|
||||
items: [{
|
||||
icon: 'fas fa-tachometer-alt',
|
||||
text: i18n.ts.dashboard,
|
||||
to: '/admin/overview',
|
||||
active: page.value === 'overview',
|
||||
}, {
|
||||
icon: 'fas fa-users',
|
||||
text: i18n.ts.users,
|
||||
to: '/admin/users',
|
||||
active: page.value === 'users',
|
||||
}, {
|
||||
icon: 'fas fa-laugh',
|
||||
text: i18n.ts.customEmojis,
|
||||
to: '/admin/emojis',
|
||||
active: page.value === 'emojis',
|
||||
}, {
|
||||
icon: 'fas fa-globe',
|
||||
text: i18n.ts.federation,
|
||||
to: '/admin/federation',
|
||||
active: page.value === 'federation',
|
||||
}, {
|
||||
icon: 'fas fa-clipboard-list',
|
||||
text: i18n.ts.jobQueue,
|
||||
to: '/admin/queue',
|
||||
active: page.value === 'queue',
|
||||
}, {
|
||||
icon: 'fas fa-cloud',
|
||||
text: i18n.ts.files,
|
||||
to: '/admin/files',
|
||||
active: page.value === 'files',
|
||||
}, {
|
||||
icon: 'fas fa-broadcast-tower',
|
||||
text: i18n.ts.announcements,
|
||||
to: '/admin/announcements',
|
||||
active: page.value === 'announcements',
|
||||
}, {
|
||||
icon: 'fas fa-audio-description',
|
||||
text: i18n.ts.ads,
|
||||
to: '/admin/ads',
|
||||
active: page.value === 'ads',
|
||||
}, {
|
||||
icon: 'fas fa-exclamation-circle',
|
||||
text: i18n.ts.abuseReports,
|
||||
to: '/admin/abuses',
|
||||
active: page.value === 'abuses',
|
||||
}],
|
||||
}, {
|
||||
title: i18n.ts.settings,
|
||||
items: [{
|
||||
icon: 'fas fa-cog',
|
||||
text: i18n.ts.general,
|
||||
to: '/admin/settings',
|
||||
active: page.value === 'settings',
|
||||
}, {
|
||||
icon: 'fas fa-envelope',
|
||||
text: i18n.ts.emailServer,
|
||||
to: '/admin/email-settings',
|
||||
active: page.value === 'email-settings',
|
||||
}, {
|
||||
icon: 'fas fa-cloud',
|
||||
text: i18n.ts.objectStorage,
|
||||
to: '/admin/object-storage',
|
||||
active: page.value === 'object-storage',
|
||||
}, {
|
||||
icon: 'fas fa-lock',
|
||||
text: i18n.ts.security,
|
||||
to: '/admin/security',
|
||||
active: page.value === 'security',
|
||||
}, {
|
||||
icon: 'fas fa-globe',
|
||||
text: i18n.ts.relays,
|
||||
to: '/admin/relays',
|
||||
active: page.value === 'relays',
|
||||
}, {
|
||||
icon: 'fas fa-share-alt',
|
||||
text: i18n.ts.integration,
|
||||
to: '/admin/integrations',
|
||||
active: page.value === 'integrations',
|
||||
}, {
|
||||
icon: 'fas fa-ban',
|
||||
text: i18n.ts.instanceBlocking,
|
||||
to: '/admin/instance-block',
|
||||
active: page.value === 'instance-block',
|
||||
}, {
|
||||
icon: 'fas fa-ghost',
|
||||
text: i18n.ts.proxyAccount,
|
||||
to: '/admin/proxy-account',
|
||||
active: page.value === 'proxy-account',
|
||||
}, {
|
||||
icon: 'fas fa-cogs',
|
||||
text: i18n.ts.other,
|
||||
to: '/admin/other-settings',
|
||||
active: page.value === 'other-settings',
|
||||
}],
|
||||
}, {
|
||||
title: i18n.ts.info,
|
||||
items: [{
|
||||
icon: 'fas fa-database',
|
||||
text: i18n.ts.database,
|
||||
to: '/admin/database',
|
||||
active: page.value === 'database',
|
||||
}],
|
||||
}]);
|
||||
const component = computed(() => {
|
||||
if (page.value == null) return null;
|
||||
switch (page.value) {
|
||||
case 'overview': return defineAsyncComponent(() => import('./overview.vue'));
|
||||
case 'users': return defineAsyncComponent(() => import('./users.vue'));
|
||||
case 'emojis': return defineAsyncComponent(() => import('./emojis.vue'));
|
||||
case 'federation': return defineAsyncComponent(() => import('../federation.vue'));
|
||||
case 'queue': return defineAsyncComponent(() => import('./queue.vue'));
|
||||
case 'files': return defineAsyncComponent(() => import('./files.vue'));
|
||||
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
|
||||
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
|
||||
case 'database': return defineAsyncComponent(() => import('./database.vue'));
|
||||
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
|
||||
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
|
||||
case 'email-settings': return defineAsyncComponent(() => import('./email-settings.vue'));
|
||||
case 'object-storage': return defineAsyncComponent(() => import('./object-storage.vue'));
|
||||
case 'security': return defineAsyncComponent(() => import('./security.vue'));
|
||||
case 'relays': return defineAsyncComponent(() => import('./relays.vue'));
|
||||
case 'integrations': return defineAsyncComponent(() => import('./integrations.vue'));
|
||||
case 'instance-block': return defineAsyncComponent(() => import('./instance-block.vue'));
|
||||
case 'proxy-account': return defineAsyncComponent(() => import('./proxy-account.vue'));
|
||||
case 'other-settings': return defineAsyncComponent(() => import('./other-settings.vue'));
|
||||
}
|
||||
}).catch(e => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
watch(component, () => {
|
||||
pageProps.value = {};
|
||||
const lookup = (ev) => {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.user,
|
||||
icon: 'fas fa-user',
|
||||
action: () => {
|
||||
lookupUser();
|
||||
}
|
||||
}, {
|
||||
text: i18n.ts.note,
|
||||
icon: 'fas fa-pencil-alt',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
}
|
||||
}, {
|
||||
text: i18n.ts.file,
|
||||
icon: 'fas fa-cloud',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
}
|
||||
}, {
|
||||
text: i18n.ts.instance,
|
||||
icon: 'fas fa-globe',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
}
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
nextTick(() => {
|
||||
scroll(el.value, { top: 0 });
|
||||
});
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => props.initialPage, () => {
|
||||
if (props.initialPage == null && !narrow.value) {
|
||||
page.value = 'overview';
|
||||
} else {
|
||||
page.value = props.initialPage;
|
||||
if (props.initialPage == null) {
|
||||
INFO.value = indexInfo;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
narrow.value = el.value.offsetWidth < 800;
|
||||
if (!narrow.value) {
|
||||
page.value = 'overview';
|
||||
}
|
||||
});
|
||||
|
||||
const invite = () => {
|
||||
os.api('admin/invite').then(x => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: x.code
|
||||
});
|
||||
}).catch(e => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const lookup = (ev) => {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.user,
|
||||
icon: 'fas fa-user',
|
||||
action: () => {
|
||||
lookupUser();
|
||||
}
|
||||
}, {
|
||||
text: i18n.ts.note,
|
||||
icon: 'fas fa-pencil-alt',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
}
|
||||
}, {
|
||||
text: i18n.ts.file,
|
||||
icon: 'fas fa-cloud',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
}
|
||||
}, {
|
||||
text: i18n.ts.instance,
|
||||
icon: 'fas fa-globe',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
}
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
};
|
||||
|
||||
return {
|
||||
[symbols.PAGE_INFO]: INFO,
|
||||
menuDef,
|
||||
header: {
|
||||
title: i18n.ts.controlPanel,
|
||||
},
|
||||
noMaintainerInformation,
|
||||
noBotProtection,
|
||||
page,
|
||||
narrow,
|
||||
view,
|
||||
el,
|
||||
pageChanged,
|
||||
childInfo,
|
||||
pageProps,
|
||||
component,
|
||||
invite,
|
||||
lookup,
|
||||
};
|
||||
},
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: INFO,
|
||||
header: {
|
||||
title: i18n.ts.controlPanel,
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -2,57 +2,45 @@
|
|||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<FormTextarea v-model="blockedHosts" class="_formBlock">
|
||||
<span>{{ $ts.blockedInstances }}</span>
|
||||
<template #caption>{{ $ts.blockedInstancesDescription }}</template>
|
||||
<span>{{ i18n.ts.blockedInstances }}</span>
|
||||
<template #caption>{{ i18n.ts.blockedInstancesDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
|
||||
<FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormButton,
|
||||
FormTextarea,
|
||||
FormSuspense,
|
||||
},
|
||||
let blockedHosts: string = $ref('');
|
||||
|
||||
emits: ['info'],
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
blockedHosts = meta.blockedHosts.join('\n');
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.instanceBlocking,
|
||||
icon: 'fas fa-ban',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
blockedHosts: '',
|
||||
}
|
||||
},
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
blockedHosts: blockedHosts.split('\n') || [],
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
this.blockedHosts = meta.blockedHosts.join('\n');
|
||||
},
|
||||
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
blockedHosts: this.blockedHosts.split('\n') || [],
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.instanceBlocking,
|
||||
icon: 'fas fa-ban',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -24,57 +24,36 @@
|
|||
</FormSuspense>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormInput,
|
||||
FormInfo,
|
||||
FormButton,
|
||||
FormSuspense,
|
||||
},
|
||||
let uri: string = $ref('');
|
||||
let enableDiscordIntegration: boolean = $ref(false);
|
||||
let discordClientId: string | null = $ref(null);
|
||||
let discordClientSecret: string | null = $ref(null);
|
||||
|
||||
emits: ['info'],
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
uri = meta.uri;
|
||||
enableDiscordIntegration = meta.enableDiscordIntegration;
|
||||
discordClientId = meta.discordClientId;
|
||||
discordClientSecret = meta.discordClientSecret;
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'Discord',
|
||||
icon: 'fab fa-discord'
|
||||
},
|
||||
enableDiscordIntegration: false,
|
||||
discordClientId: null,
|
||||
discordClientSecret: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
this.uri = meta.uri;
|
||||
this.enableDiscordIntegration = meta.enableDiscordIntegration;
|
||||
this.discordClientId = meta.discordClientId;
|
||||
this.discordClientSecret = meta.discordClientSecret;
|
||||
},
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
enableDiscordIntegration: this.enableDiscordIntegration,
|
||||
discordClientId: this.discordClientId,
|
||||
discordClientSecret: this.discordClientSecret,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
enableDiscordIntegration,
|
||||
discordClientId,
|
||||
discordClientSecret,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -24,57 +24,36 @@
|
|||
</FormSuspense>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormInput,
|
||||
FormInfo,
|
||||
FormButton,
|
||||
FormSuspense,
|
||||
},
|
||||
let uri: string = $ref('');
|
||||
let enableGithubIntegration: boolean = $ref(false);
|
||||
let githubClientId: string | null = $ref(null);
|
||||
let githubClientSecret: string | null = $ref(null);
|
||||
|
||||
emits: ['info'],
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
uri = meta.uri;
|
||||
enableGithubIntegration = meta.enableGithubIntegration;
|
||||
githubClientId = meta.githubClientId;
|
||||
githubClientSecret = meta.githubClientSecret;
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'GitHub',
|
||||
icon: 'fab fa-github'
|
||||
},
|
||||
enableGithubIntegration: false,
|
||||
githubClientId: null,
|
||||
githubClientSecret: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
this.uri = meta.uri;
|
||||
this.enableGithubIntegration = meta.enableGithubIntegration;
|
||||
this.githubClientId = meta.githubClientId;
|
||||
this.githubClientSecret = meta.githubClientSecret;
|
||||
},
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
enableGithubIntegration: this.enableGithubIntegration,
|
||||
githubClientId: this.githubClientId,
|
||||
githubClientSecret: this.githubClientSecret,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
enableGithubIntegration,
|
||||
githubClientId,
|
||||
githubClientSecret,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
</FormSuspense>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { defineComponent } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
|
@ -32,49 +32,28 @@ import FormButton from '@/components/ui/button.vue';
|
|||
import FormInfo from '@/components/ui/info.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormInput,
|
||||
FormInfo,
|
||||
FormButton,
|
||||
FormSuspense,
|
||||
},
|
||||
let uri: string = $ref('');
|
||||
let enableTwitterIntegration: boolean = $ref(false);
|
||||
let twitterConsumerKey: string | null = $ref(null);
|
||||
let twitterConsumerSecret: string | null = $ref(null);
|
||||
|
||||
emits: ['info'],
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
uri = meta.uri;
|
||||
enableTwitterIntegration = meta.enableTwitterIntegration;
|
||||
twitterConsumerKey = meta.twitterConsumerKey;
|
||||
twitterConsumerSecret = meta.twitterConsumerSecret;
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: 'Twitter',
|
||||
icon: 'fab fa-twitter'
|
||||
},
|
||||
enableTwitterIntegration: false,
|
||||
twitterConsumerKey: null,
|
||||
twitterConsumerSecret: null,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
this.uri = meta.uri;
|
||||
this.enableTwitterIntegration = meta.enableTwitterIntegration;
|
||||
this.twitterConsumerKey = meta.twitterConsumerKey;
|
||||
this.twitterConsumerSecret = meta.twitterConsumerSecret;
|
||||
},
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
enableTwitterIntegration: this.enableTwitterIntegration,
|
||||
twitterConsumerKey: this.twitterConsumerKey,
|
||||
twitterConsumerSecret: this.twitterConsumerSecret,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
enableTwitterIntegration,
|
||||
twitterConsumerKey,
|
||||
twitterConsumerSecret,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -4,69 +4,52 @@
|
|||
<FormFolder class="_formBlock">
|
||||
<template #icon><i class="fab fa-twitter"></i></template>
|
||||
<template #label>Twitter</template>
|
||||
<template #suffix>{{ enableTwitterIntegration ? $ts.enabled : $ts.disabled }}</template>
|
||||
<template #suffix>{{ enableTwitterIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
||||
<XTwitter/>
|
||||
</FormFolder>
|
||||
<FormFolder to="/admin/integrations/github" class="_formBlock">
|
||||
<FormFolder class="_formBlock">
|
||||
<template #icon><i class="fab fa-github"></i></template>
|
||||
<template #label>GitHub</template>
|
||||
<template #suffix>{{ enableGithubIntegration ? $ts.enabled : $ts.disabled }}</template>
|
||||
<template #suffix>{{ enableGithubIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
||||
<XGithub/>
|
||||
</FormFolder>
|
||||
<FormFolder to="/admin/integrations/discord" class="_formBlock">
|
||||
<FormFolder class="_formBlock">
|
||||
<template #icon><i class="fab fa-discord"></i></template>
|
||||
<template #label>Discord</template>
|
||||
<template #suffix>{{ enableDiscordIntegration ? $ts.enabled : $ts.disabled }}</template>
|
||||
<template #suffix>{{ enableDiscordIntegration ? i18n.ts.enabled : i18n.ts.disabled }}</template>
|
||||
<XDiscord/>
|
||||
</FormFolder>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import FormFolder from '@/components/form/folder.vue';
|
||||
import FormSecion from '@/components/form/section.vue';
|
||||
import FormSuspense from '@/components/form/suspense.vue';
|
||||
import XTwitter from './integrations.twitter.vue';
|
||||
import XGithub from './integrations.github.vue';
|
||||
import XDiscord from './integrations.discord.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormFolder,
|
||||
FormSecion,
|
||||
FormSuspense,
|
||||
XTwitter,
|
||||
XGithub,
|
||||
XDiscord,
|
||||
},
|
||||
let enableTwitterIntegration: boolean = $ref(false);
|
||||
let enableGithubIntegration: boolean = $ref(false);
|
||||
let enableDiscordIntegration: boolean = $ref(false);
|
||||
|
||||
emits: ['info'],
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
enableTwitterIntegration = meta.enableTwitterIntegration;
|
||||
enableGithubIntegration = meta.enableGithubIntegration;
|
||||
enableDiscordIntegration = meta.enableDiscordIntegration;
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.integration,
|
||||
icon: 'fas fa-share-alt',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
enableTwitterIntegration: false,
|
||||
enableGithubIntegration: false,
|
||||
enableDiscordIntegration: false,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
this.enableTwitterIntegration = meta.enableTwitterIntegration;
|
||||
this.enableGithubIntegration = meta.enableGithubIntegration;
|
||||
this.enableDiscordIntegration = meta.enableDiscordIntegration;
|
||||
},
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.integration,
|
||||
icon: 'fas fa-share-alt',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -2,32 +2,32 @@
|
|||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ $ts.useObjectStorage }}</FormSwitch>
|
||||
<FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch>
|
||||
|
||||
<template v-if="useObjectStorage">
|
||||
<FormInput v-model="objectStorageBaseUrl" class="_formBlock">
|
||||
<template #label>{{ $ts.objectStorageBaseUrl }}</template>
|
||||
<template #caption>{{ $ts.objectStorageBaseUrlDesc }}</template>
|
||||
<template #label>{{ i18n.ts.objectStorageBaseUrl }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="objectStorageBucket" class="_formBlock">
|
||||
<template #label>{{ $ts.objectStorageBucket }}</template>
|
||||
<template #caption>{{ $ts.objectStorageBucketDesc }}</template>
|
||||
<template #label>{{ i18n.ts.objectStorageBucket }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="objectStoragePrefix" class="_formBlock">
|
||||
<template #label>{{ $ts.objectStoragePrefix }}</template>
|
||||
<template #caption>{{ $ts.objectStoragePrefixDesc }}</template>
|
||||
<template #label>{{ i18n.ts.objectStoragePrefix }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="objectStorageEndpoint" class="_formBlock">
|
||||
<template #label>{{ $ts.objectStorageEndpoint }}</template>
|
||||
<template #caption>{{ $ts.objectStorageEndpointDesc }}</template>
|
||||
<template #label>{{ i18n.ts.objectStorageEndpoint }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="objectStorageRegion" class="_formBlock">
|
||||
<template #label>{{ $ts.objectStorageRegion }}</template>
|
||||
<template #caption>{{ $ts.objectStorageRegionDesc }}</template>
|
||||
<template #label>{{ i18n.ts.objectStorageRegion }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormSplit :min-width="280">
|
||||
|
@ -43,17 +43,17 @@
|
|||
</FormSplit>
|
||||
|
||||
<FormSwitch v-model="objectStorageUseSSL" class="_formBlock">
|
||||
<template #label>{{ $ts.objectStorageUseSSL }}</template>
|
||||
<template #caption>{{ $ts.objectStorageUseSSLDesc }}</template>
|
||||
<template #label>{{ i18n.ts.objectStorageUseSSL }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="objectStorageUseProxy" class="_formBlock">
|
||||
<template #label>{{ $ts.objectStorageUseProxy }}</template>
|
||||
<template #caption>{{ $ts.objectStorageUseProxyDesc }}</template>
|
||||
<template #label>{{ i18n.ts.objectStorageUseProxy }}</template>
|
||||
<template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock">
|
||||
<template #label>{{ $ts.objectStorageSetPublicRead }}</template>
|
||||
<template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock">
|
||||
|
@ -65,8 +65,8 @@
|
|||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormGroup from '@/components/form/group.vue';
|
||||
|
@ -76,84 +76,70 @@ import FormSection from '@/components/form/section.vue';
|
|||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormInput,
|
||||
FormGroup,
|
||||
FormSuspense,
|
||||
FormSplit,
|
||||
FormSection,
|
||||
},
|
||||
let useObjectStorage: boolean = $ref(false);
|
||||
let objectStorageBaseUrl: string | null = $ref(null);
|
||||
let objectStorageBucket: string | null = $ref(null);
|
||||
let objectStoragePrefix: string | null = $ref(null);
|
||||
let objectStorageEndpoint: string | null = $ref(null);
|
||||
let objectStorageRegion: string | null = $ref(null);
|
||||
let objectStoragePort: number | null = $ref(null);
|
||||
let objectStorageAccessKey: string | null = $ref(null);
|
||||
let objectStorageSecretKey: string | null = $ref(null);
|
||||
let objectStorageUseSSL: boolean = $ref(false);
|
||||
let objectStorageUseProxy: boolean = $ref(false);
|
||||
let objectStorageSetPublicRead: boolean = $ref(false);
|
||||
let objectStorageS3ForcePathStyle: boolean = $ref(true);
|
||||
|
||||
emits: ['info'],
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
useObjectStorage = meta.useObjectStorage;
|
||||
objectStorageBaseUrl = meta.objectStorageBaseUrl;
|
||||
objectStorageBucket = meta.objectStorageBucket;
|
||||
objectStoragePrefix = meta.objectStoragePrefix;
|
||||
objectStorageEndpoint = meta.objectStorageEndpoint;
|
||||
objectStorageRegion = meta.objectStorageRegion;
|
||||
objectStoragePort = meta.objectStoragePort;
|
||||
objectStorageAccessKey = meta.objectStorageAccessKey;
|
||||
objectStorageSecretKey = meta.objectStorageSecretKey;
|
||||
objectStorageUseSSL = meta.objectStorageUseSSL;
|
||||
objectStorageUseProxy = meta.objectStorageUseProxy;
|
||||
objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
|
||||
objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.objectStorage,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: this.$ts.save,
|
||||
handler: this.save,
|
||||
}],
|
||||
},
|
||||
useObjectStorage: false,
|
||||
objectStorageBaseUrl: null,
|
||||
objectStorageBucket: null,
|
||||
objectStoragePrefix: null,
|
||||
objectStorageEndpoint: null,
|
||||
objectStorageRegion: null,
|
||||
objectStoragePort: null,
|
||||
objectStorageAccessKey: null,
|
||||
objectStorageSecretKey: null,
|
||||
objectStorageUseSSL: false,
|
||||
objectStorageUseProxy: false,
|
||||
objectStorageSetPublicRead: false,
|
||||
objectStorageS3ForcePathStyle: true,
|
||||
}
|
||||
},
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
useObjectStorage,
|
||||
objectStorageBaseUrl,
|
||||
objectStorageBucket,
|
||||
objectStoragePrefix,
|
||||
objectStorageEndpoint,
|
||||
objectStorageRegion,
|
||||
objectStoragePort,
|
||||
objectStorageAccessKey,
|
||||
objectStorageSecretKey,
|
||||
objectStorageUseSSL,
|
||||
objectStorageUseProxy,
|
||||
objectStorageSetPublicRead,
|
||||
objectStorageS3ForcePathStyle,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
this.useObjectStorage = meta.useObjectStorage;
|
||||
this.objectStorageBaseUrl = meta.objectStorageBaseUrl;
|
||||
this.objectStorageBucket = meta.objectStorageBucket;
|
||||
this.objectStoragePrefix = meta.objectStoragePrefix;
|
||||
this.objectStorageEndpoint = meta.objectStorageEndpoint;
|
||||
this.objectStorageRegion = meta.objectStorageRegion;
|
||||
this.objectStoragePort = meta.objectStoragePort;
|
||||
this.objectStorageAccessKey = meta.objectStorageAccessKey;
|
||||
this.objectStorageSecretKey = meta.objectStorageSecretKey;
|
||||
this.objectStorageUseSSL = meta.objectStorageUseSSL;
|
||||
this.objectStorageUseProxy = meta.objectStorageUseProxy;
|
||||
this.objectStorageSetPublicRead = meta.objectStorageSetPublicRead;
|
||||
this.objectStorageS3ForcePathStyle = meta.objectStorageS3ForcePathStyle;
|
||||
},
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
useObjectStorage: this.useObjectStorage,
|
||||
objectStorageBaseUrl: this.objectStorageBaseUrl ? this.objectStorageBaseUrl : null,
|
||||
objectStorageBucket: this.objectStorageBucket ? this.objectStorageBucket : null,
|
||||
objectStoragePrefix: this.objectStoragePrefix ? this.objectStoragePrefix : null,
|
||||
objectStorageEndpoint: this.objectStorageEndpoint ? this.objectStorageEndpoint : null,
|
||||
objectStorageRegion: this.objectStorageRegion ? this.objectStorageRegion : null,
|
||||
objectStoragePort: this.objectStoragePort ? this.objectStoragePort : null,
|
||||
objectStorageAccessKey: this.objectStorageAccessKey ? this.objectStorageAccessKey : null,
|
||||
objectStorageSecretKey: this.objectStorageSecretKey ? this.objectStorageSecretKey : null,
|
||||
objectStorageUseSSL: this.objectStorageUseSSL,
|
||||
objectStorageUseProxy: this.objectStorageUseProxy,
|
||||
objectStorageSetPublicRead: this.objectStorageSetPublicRead,
|
||||
objectStorageS3ForcePathStyle: this.objectStorageS3ForcePathStyle,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.objectStorage,
|
||||
icon: 'fas fa-cloud',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}],
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
<template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormSuspense :p="init">
|
||||
<MkInfo class="_formBlock">{{ $ts.proxyAccountDescription }}</MkInfo>
|
||||
<MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo>
|
||||
<MkKeyValue class="_formBlock">
|
||||
<template #key>{{ $ts.proxyAccount }}</template>
|
||||
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : $ts.none }}</template>
|
||||
<template #key>{{ i18n.ts.proxyAccount }}</template>
|
||||
<template #value>{{ proxyAccount ? `@${proxyAccount.username}` : i18n.ts.none }}</template>
|
||||
</MkKeyValue>
|
||||
|
||||
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ $ts.selectAccount }}</FormButton>
|
||||
<FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton>
|
||||
</FormSuspense>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkKeyValue from '@/components/key-value.vue';
|
||||
import FormButton from '@/components/ui/button.vue';
|
||||
import MkInfo from '@/components/ui/info.vue';
|
||||
|
@ -21,53 +21,40 @@ import FormSuspense from '@/components/form/suspense.vue';
|
|||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkKeyValue,
|
||||
FormButton,
|
||||
MkInfo,
|
||||
FormSuspense,
|
||||
},
|
||||
let proxyAccount: any = $ref(null);
|
||||
let proxyAccountId: any = $ref(null);
|
||||
|
||||
emits: ['info'],
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
proxyAccountId = meta.proxyAccountId;
|
||||
if (proxyAccountId) {
|
||||
proxyAccount = await os.api('users/show', { userId: proxyAccountId });
|
||||
}
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.proxyAccount,
|
||||
icon: 'fas fa-ghost',
|
||||
bg: 'var(--bg)',
|
||||
},
|
||||
proxyAccount: null,
|
||||
proxyAccountId: null,
|
||||
}
|
||||
},
|
||||
function chooseProxyAccount() {
|
||||
os.selectUser().then(user => {
|
||||
proxyAccount = user;
|
||||
proxyAccountId = user.id;
|
||||
save();
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
this.proxyAccountId = meta.proxyAccountId;
|
||||
if (this.proxyAccountId) {
|
||||
this.proxyAccount = await os.api('users/show', { userId: this.proxyAccountId });
|
||||
}
|
||||
},
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
proxyAccountId: proxyAccountId,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
|
||||
chooseProxyAccount() {
|
||||
os.selectUser().then(user => {
|
||||
this.proxyAccount = user;
|
||||
this.proxyAccountId = user.id;
|
||||
this.save();
|
||||
});
|
||||
},
|
||||
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
proxyAccountId: this.proxyAccountId,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.proxyAccount,
|
||||
icon: 'fas fa-ghost',
|
||||
bg: 'var(--bg)',
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -26,62 +26,40 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw, onMounted, onUnmounted, ref } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import number from '@/filters/number';
|
||||
import MkQueueChart from '@/components/queue-chart.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkQueueChart
|
||||
},
|
||||
const activeSincePrevTick = ref(0);
|
||||
const active = ref(0);
|
||||
const waiting = ref(0);
|
||||
const delayed = ref(0);
|
||||
const jobs = ref([]);
|
||||
|
||||
props: {
|
||||
domain: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
connection: {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
const props = defineProps<{
|
||||
domain: string,
|
||||
connection: any,
|
||||
}>();
|
||||
|
||||
setup(props) {
|
||||
const activeSincePrevTick = ref(0);
|
||||
const active = ref(0);
|
||||
const waiting = ref(0);
|
||||
const delayed = ref(0);
|
||||
const jobs = ref([]);
|
||||
onMounted(() => {
|
||||
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
|
||||
jobs.value = result;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
os.api(props.domain === 'inbox' ? 'admin/queue/inbox-delayed' : props.domain === 'deliver' ? 'admin/queue/deliver-delayed' : null, {}).then(result => {
|
||||
jobs.value = result;
|
||||
});
|
||||
const onStats = (stats) => {
|
||||
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
|
||||
active.value = stats[props.domain].active;
|
||||
waiting.value = stats[props.domain].waiting;
|
||||
delayed.value = stats[props.domain].delayed;
|
||||
};
|
||||
|
||||
const onStats = (stats) => {
|
||||
activeSincePrevTick.value = stats[props.domain].activeSincePrevTick;
|
||||
active.value = stats[props.domain].active;
|
||||
waiting.value = stats[props.domain].waiting;
|
||||
delayed.value = stats[props.domain].delayed;
|
||||
};
|
||||
props.connection.on('stats', onStats);
|
||||
|
||||
props.connection.on('stats', onStats);
|
||||
|
||||
onUnmounted(() => {
|
||||
props.connection.off('stats', onStats);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
jobs,
|
||||
activeSincePrevTick,
|
||||
active,
|
||||
waiting,
|
||||
delayed,
|
||||
number,
|
||||
};
|
||||
},
|
||||
onUnmounted(() => {
|
||||
props.connection.off('stats', onStats);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -6,71 +6,60 @@
|
|||
<XQueue :connection="connection" domain="deliver">
|
||||
<template #title>Out</template>
|
||||
</XQueue>
|
||||
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ $ts.clearQueue }}</MkButton>
|
||||
<MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import XQueue from './queue.chart.vue';
|
||||
import * as os from '@/os';
|
||||
import { stream } from '@/stream';
|
||||
import * as symbols from '@/symbols';
|
||||
import * as config from '@/config';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
XQueue,
|
||||
},
|
||||
const connection = markRaw(stream.useChannel('queueStats'))
|
||||
|
||||
emits: ['info'],
|
||||
function clear() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
title: i18n.ts.clearQueueConfirmTitle,
|
||||
text: i18n.ts.clearQueueConfirmText,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.jobQueue,
|
||||
icon: 'fas fa-clipboard-list',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-up-right-from-square',
|
||||
text: this.$ts.dashboard,
|
||||
handler: () => {
|
||||
window.open(config.url + '/queue', '_blank');
|
||||
},
|
||||
}],
|
||||
},
|
||||
connection: markRaw(stream.useChannel('queueStats')),
|
||||
}
|
||||
},
|
||||
os.apiWithDialog('admin/queue/clear');
|
||||
});
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 200
|
||||
});
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 200
|
||||
});
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
beforeUnmount() {
|
||||
this.connection.dispose();
|
||||
},
|
||||
onBeforeUnmount(() => {
|
||||
connection.dispose();
|
||||
});
|
||||
|
||||
methods: {
|
||||
clear() {
|
||||
os.confirm({
|
||||
type: 'warning',
|
||||
title: this.$ts.clearQueueConfirmTitle,
|
||||
text: this.$ts.clearQueueConfirmText,
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
|
||||
os.apiWithDialog('admin/queue/clear', {});
|
||||
});
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.jobQueue,
|
||||
icon: 'fas fa-clipboard-list',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-up-right-from-square',
|
||||
text: i18n.ts.dashboard,
|
||||
handler: () => {
|
||||
window.open(config.url + '/queue', '_blank');
|
||||
},
|
||||
}],
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -8,84 +8,71 @@
|
|||
<i v-else class="fas fa-clock icon requesting"></i>
|
||||
<span>{{ $t(`_relayStatus.${relay.status}`) }}</span>
|
||||
</div>
|
||||
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ $ts.remove }}</MkButton>
|
||||
<MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkButton,
|
||||
},
|
||||
let relays: any[] = $ref([]);
|
||||
|
||||
emits: ['info'],
|
||||
async function addRelay() {
|
||||
const { canceled, result: inbox } = await os.inputText({
|
||||
title: i18n.ts.addRelay,
|
||||
type: 'url',
|
||||
placeholder: i18n.ts.inboxUrl
|
||||
});
|
||||
if (canceled) return;
|
||||
os.api('admin/relays/add', {
|
||||
inbox
|
||||
}).then((relay: any) => {
|
||||
refresh();
|
||||
}).catch((err: any) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.message || err
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.relays,
|
||||
icon: 'fas fa-globe',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: this.$ts.addRelay,
|
||||
handler: this.addRelay,
|
||||
}],
|
||||
},
|
||||
relays: [],
|
||||
inbox: '',
|
||||
}
|
||||
},
|
||||
function remove(inbox: string) {
|
||||
os.api('admin/relays/remove', {
|
||||
inbox
|
||||
}).then(() => {
|
||||
refresh();
|
||||
}).catch((err: any) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: err.message || err
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
created() {
|
||||
this.refresh();
|
||||
},
|
||||
function refresh() {
|
||||
os.api('admin/relays/list').then((relayList: any) => {
|
||||
relays = relayList;
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
async addRelay() {
|
||||
const { canceled, result: inbox } = await os.inputText({
|
||||
title: this.$ts.addRelay,
|
||||
type: 'url',
|
||||
placeholder: this.$ts.inboxUrl
|
||||
});
|
||||
if (canceled) return;
|
||||
os.api('admin/relays/add', {
|
||||
inbox
|
||||
}).then((relay: any) => {
|
||||
this.refresh();
|
||||
}).catch((e: any) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e.message || e
|
||||
});
|
||||
});
|
||||
},
|
||||
refresh();
|
||||
|
||||
remove(inbox: string) {
|
||||
os.api('admin/relays/remove', {
|
||||
inbox
|
||||
}).then(() => {
|
||||
this.refresh();
|
||||
}).catch((e: any) => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: e.message || e
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
refresh() {
|
||||
os.api('admin/relays/list').then((relays: any) => {
|
||||
this.relays = relays;
|
||||
});
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.relays,
|
||||
icon: 'fas fa-globe',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-plus',
|
||||
text: i18n.ts.addRelay,
|
||||
handler: addRelay,
|
||||
}],
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -3,104 +3,104 @@
|
|||
<FormSuspense :p="init">
|
||||
<div class="_formRoot">
|
||||
<FormInput v-model="name" class="_formBlock">
|
||||
<template #label>{{ $ts.instanceName }}</template>
|
||||
<template #label>{{ i18n.ts.instanceName }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormTextarea v-model="description" class="_formBlock">
|
||||
<template #label>{{ $ts.instanceDescription }}</template>
|
||||
<template #label>{{ i18n.ts.instanceDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormInput v-model="tosUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ $ts.tosUrl }}</template>
|
||||
<template #label>{{ i18n.ts.tosUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormSplit :min-width="300">
|
||||
<FormInput v-model="maintainerName" class="_formBlock">
|
||||
<template #label>{{ $ts.maintainerName }}</template>
|
||||
<template #label>{{ i18n.ts.maintainerName }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="maintainerEmail" type="email" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-envelope"></i></template>
|
||||
<template #label>{{ $ts.maintainerEmail }}</template>
|
||||
<template #label>{{ i18n.ts.maintainerEmail }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
|
||||
<FormTextarea v-model="pinnedUsers" class="_formBlock">
|
||||
<template #label>{{ $ts.pinnedUsers }}</template>
|
||||
<template #caption>{{ $ts.pinnedUsersDescription }}</template>
|
||||
<template #label>{{ i18n.ts.pinnedUsers }}</template>
|
||||
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormSection>
|
||||
<FormSwitch v-model="enableRegistration" class="_formBlock">
|
||||
<template #label>{{ $ts.enableRegistration }}</template>
|
||||
<template #label>{{ i18n.ts.enableRegistration }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSwitch v-model="emailRequiredForSignup" class="_formBlock">
|
||||
<template #label>{{ $ts.emailRequiredForSignup }}</template>
|
||||
<template #label>{{ i18n.ts.emailRequiredForSignup }}</template>
|
||||
</FormSwitch>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ $ts.enableLocalTimeline }}</FormSwitch>
|
||||
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ $ts.enableGlobalTimeline }}</FormSwitch>
|
||||
<FormInfo class="_formBlock">{{ $ts.disablingTimelinesInfo }}</FormInfo>
|
||||
<FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch>
|
||||
<FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch>
|
||||
<FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.theme }}</template>
|
||||
<template #label>{{ i18n.ts.theme }}</template>
|
||||
|
||||
<FormInput v-model="iconUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ $ts.iconUrl }}</template>
|
||||
<template #label>{{ i18n.ts.iconUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="bannerUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ $ts.bannerUrl }}</template>
|
||||
<template #label>{{ i18n.ts.bannerUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="backgroundImageUrl" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-link"></i></template>
|
||||
<template #label>{{ $ts.backgroundImageUrl }}</template>
|
||||
<template #label>{{ i18n.ts.backgroundImageUrl }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="themeColor" class="_formBlock">
|
||||
<template #prefix><i class="fas fa-palette"></i></template>
|
||||
<template #label>{{ $ts.themeColor }}</template>
|
||||
<template #label>{{ i18n.ts.themeColor }}</template>
|
||||
<template #caption>#RRGGBB</template>
|
||||
</FormInput>
|
||||
|
||||
<FormTextarea v-model="defaultLightTheme" class="_formBlock">
|
||||
<template #label>{{ $ts.instanceDefaultLightTheme }}</template>
|
||||
<template #caption>{{ $ts.instanceDefaultThemeDescription }}</template>
|
||||
<template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template>
|
||||
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
|
||||
</FormTextarea>
|
||||
|
||||
<FormTextarea v-model="defaultDarkTheme" class="_formBlock">
|
||||
<template #label>{{ $ts.instanceDefaultDarkTheme }}</template>
|
||||
<template #caption>{{ $ts.instanceDefaultThemeDescription }}</template>
|
||||
<template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template>
|
||||
<template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template>
|
||||
</FormTextarea>
|
||||
</FormSection>
|
||||
|
||||
<FormSection>
|
||||
<template #label>{{ $ts.files }}</template>
|
||||
<template #label>{{ i18n.ts.files }}</template>
|
||||
|
||||
<FormSwitch v-model="cacheRemoteFiles" class="_formBlock">
|
||||
<template #label>{{ $ts.cacheRemoteFiles }}</template>
|
||||
<template #caption>{{ $ts.cacheRemoteFilesDescription }}</template>
|
||||
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
|
||||
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<FormSplit :min-width="280">
|
||||
<FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock">
|
||||
<template #label>{{ $ts.driveCapacityPerLocalAccount }}</template>
|
||||
<template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
|
||||
<template #suffix>MB</template>
|
||||
<template #caption>{{ $ts.inMb }}</template>
|
||||
<template #caption>{{ i18n.ts.inMb }}</template>
|
||||
</FormInput>
|
||||
|
||||
<FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock">
|
||||
<template #label>{{ $ts.driveCapacityPerRemoteAccount }}</template>
|
||||
<template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
|
||||
<template #suffix>MB</template>
|
||||
<template #caption>{{ $ts.inMb }}</template>
|
||||
<template #caption>{{ i18n.ts.inMb }}</template>
|
||||
</FormInput>
|
||||
</FormSplit>
|
||||
</FormSection>
|
||||
|
@ -109,8 +109,8 @@
|
|||
<template #label>ServiceWorker</template>
|
||||
|
||||
<FormSwitch v-model="enableServiceWorker" class="_formBlock">
|
||||
<template #label>{{ $ts.enableServiceworker }}</template>
|
||||
<template #caption>{{ $ts.serviceworkerInfo }}</template>
|
||||
<template #label>{{ i18n.ts.enableServiceworker }}</template>
|
||||
<template #caption>{{ i18n.ts.serviceworkerInfo }}</template>
|
||||
</FormSwitch>
|
||||
|
||||
<template v-if="enableServiceWorker">
|
||||
|
@ -142,8 +142,8 @@
|
|||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import FormSwitch from '@/components/form/switch.vue';
|
||||
import FormInput from '@/components/form/input.vue';
|
||||
import FormTextarea from '@/components/form/textarea.vue';
|
||||
|
@ -154,119 +154,103 @@ import FormSuspense from '@/components/form/suspense.vue';
|
|||
import * as os from '@/os';
|
||||
import * as symbols from '@/symbols';
|
||||
import { fetchInstance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
FormSwitch,
|
||||
FormInput,
|
||||
FormSuspense,
|
||||
FormTextarea,
|
||||
FormInfo,
|
||||
FormSection,
|
||||
FormSplit,
|
||||
},
|
||||
let name: string | null = $ref(null);
|
||||
let description: string | null = $ref(null);
|
||||
let tosUrl: string | null = $ref(null);
|
||||
let maintainerName: string | null = $ref(null);
|
||||
let maintainerEmail: string | null = $ref(null);
|
||||
let iconUrl: string | null = $ref(null);
|
||||
let bannerUrl: string | null = $ref(null);
|
||||
let backgroundImageUrl: string | null = $ref(null);
|
||||
let themeColor: any = $ref(null);
|
||||
let defaultLightTheme: any = $ref(null);
|
||||
let defaultDarkTheme: any = $ref(null);
|
||||
let enableLocalTimeline: boolean = $ref(false);
|
||||
let enableGlobalTimeline: boolean = $ref(false);
|
||||
let pinnedUsers: string = $ref('');
|
||||
let cacheRemoteFiles: boolean = $ref(false);
|
||||
let localDriveCapacityMb: any = $ref(0);
|
||||
let remoteDriveCapacityMb: any = $ref(0);
|
||||
let enableRegistration: boolean = $ref(false);
|
||||
let emailRequiredForSignup: boolean = $ref(false);
|
||||
let enableServiceWorker: boolean = $ref(false);
|
||||
let swPublicKey: any = $ref(null);
|
||||
let swPrivateKey: any = $ref(null);
|
||||
let deeplAuthKey: string = $ref('');
|
||||
let deeplIsPro: boolean = $ref(false);
|
||||
|
||||
emits: ['info'],
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
name = meta.name;
|
||||
description = meta.description;
|
||||
tosUrl = meta.tosUrl;
|
||||
iconUrl = meta.iconUrl;
|
||||
bannerUrl = meta.bannerUrl;
|
||||
backgroundImageUrl = meta.backgroundImageUrl;
|
||||
themeColor = meta.themeColor;
|
||||
defaultLightTheme = meta.defaultLightTheme;
|
||||
defaultDarkTheme = meta.defaultDarkTheme;
|
||||
maintainerName = meta.maintainerName;
|
||||
maintainerEmail = meta.maintainerEmail;
|
||||
enableLocalTimeline = !meta.disableLocalTimeline;
|
||||
enableGlobalTimeline = !meta.disableGlobalTimeline;
|
||||
pinnedUsers = meta.pinnedUsers.join('\n');
|
||||
cacheRemoteFiles = meta.cacheRemoteFiles;
|
||||
localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
|
||||
remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
|
||||
enableRegistration = !meta.disableRegistration;
|
||||
emailRequiredForSignup = meta.emailRequiredForSignup;
|
||||
enableServiceWorker = meta.enableServiceWorker;
|
||||
swPublicKey = meta.swPublickey;
|
||||
swPrivateKey = meta.swPrivateKey;
|
||||
deeplAuthKey = meta.deeplAuthKey;
|
||||
deeplIsPro = meta.deeplIsPro;
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.general,
|
||||
icon: 'fas fa-cog',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: this.$ts.save,
|
||||
handler: this.save,
|
||||
}],
|
||||
},
|
||||
name: null,
|
||||
description: null,
|
||||
tosUrl: null as string | null,
|
||||
maintainerName: null,
|
||||
maintainerEmail: null,
|
||||
iconUrl: null,
|
||||
bannerUrl: null,
|
||||
backgroundImageUrl: null,
|
||||
themeColor: null,
|
||||
defaultLightTheme: null,
|
||||
defaultDarkTheme: null,
|
||||
enableLocalTimeline: false,
|
||||
enableGlobalTimeline: false,
|
||||
pinnedUsers: '',
|
||||
cacheRemoteFiles: false,
|
||||
localDriveCapacityMb: 0,
|
||||
remoteDriveCapacityMb: 0,
|
||||
enableRegistration: false,
|
||||
emailRequiredForSignup: false,
|
||||
enableServiceWorker: false,
|
||||
swPublicKey: null,
|
||||
swPrivateKey: null,
|
||||
deeplAuthKey: '',
|
||||
deeplIsPro: false,
|
||||
}
|
||||
},
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
name,
|
||||
description,
|
||||
tosUrl,
|
||||
iconUrl,
|
||||
bannerUrl,
|
||||
backgroundImageUrl,
|
||||
themeColor: themeColor === '' ? null : themeColor,
|
||||
defaultLightTheme: defaultLightTheme === '' ? null : defaultLightTheme,
|
||||
defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme,
|
||||
maintainerName,
|
||||
maintainerEmail,
|
||||
disableLocalTimeline: !enableLocalTimeline,
|
||||
disableGlobalTimeline: !enableGlobalTimeline,
|
||||
pinnedUsers: pinnedUsers.split('\n'),
|
||||
cacheRemoteFiles,
|
||||
localDriveCapacityMb: parseInt(localDriveCapacityMb, 10),
|
||||
remoteDriveCapacityMb: parseInt(remoteDriveCapacityMb, 10),
|
||||
disableRegistration: !enableRegistration,
|
||||
emailRequiredForSignup,
|
||||
enableServiceWorker,
|
||||
swPublicKey,
|
||||
swPrivateKey,
|
||||
deeplAuthKey,
|
||||
deeplIsPro,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
|
||||
methods: {
|
||||
async init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
this.name = meta.name;
|
||||
this.description = meta.description;
|
||||
this.tosUrl = meta.tosUrl;
|
||||
this.iconUrl = meta.iconUrl;
|
||||
this.bannerUrl = meta.bannerUrl;
|
||||
this.backgroundImageUrl = meta.backgroundImageUrl;
|
||||
this.themeColor = meta.themeColor;
|
||||
this.defaultLightTheme = meta.defaultLightTheme;
|
||||
this.defaultDarkTheme = meta.defaultDarkTheme;
|
||||
this.maintainerName = meta.maintainerName;
|
||||
this.maintainerEmail = meta.maintainerEmail;
|
||||
this.enableLocalTimeline = !meta.disableLocalTimeline;
|
||||
this.enableGlobalTimeline = !meta.disableGlobalTimeline;
|
||||
this.pinnedUsers = meta.pinnedUsers.join('\n');
|
||||
this.cacheRemoteFiles = meta.cacheRemoteFiles;
|
||||
this.localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
|
||||
this.remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
|
||||
this.enableRegistration = !meta.disableRegistration;
|
||||
this.emailRequiredForSignup = meta.emailRequiredForSignup;
|
||||
this.enableServiceWorker = meta.enableServiceWorker;
|
||||
this.swPublicKey = meta.swPublickey;
|
||||
this.swPrivateKey = meta.swPrivateKey;
|
||||
this.deeplAuthKey = meta.deeplAuthKey;
|
||||
this.deeplIsPro = meta.deeplIsPro;
|
||||
},
|
||||
|
||||
save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
tosUrl: this.tosUrl,
|
||||
iconUrl: this.iconUrl,
|
||||
bannerUrl: this.bannerUrl,
|
||||
backgroundImageUrl: this.backgroundImageUrl,
|
||||
themeColor: this.themeColor === '' ? null : this.themeColor,
|
||||
defaultLightTheme: this.defaultLightTheme === '' ? null : this.defaultLightTheme,
|
||||
defaultDarkTheme: this.defaultDarkTheme === '' ? null : this.defaultDarkTheme,
|
||||
maintainerName: this.maintainerName,
|
||||
maintainerEmail: this.maintainerEmail,
|
||||
disableLocalTimeline: !this.enableLocalTimeline,
|
||||
disableGlobalTimeline: !this.enableGlobalTimeline,
|
||||
pinnedUsers: this.pinnedUsers.split('\n'),
|
||||
cacheRemoteFiles: this.cacheRemoteFiles,
|
||||
localDriveCapacityMb: parseInt(this.localDriveCapacityMb, 10),
|
||||
remoteDriveCapacityMb: parseInt(this.remoteDriveCapacityMb, 10),
|
||||
disableRegistration: !this.enableRegistration,
|
||||
emailRequiredForSignup: this.emailRequiredForSignup,
|
||||
enableServiceWorker: this.enableServiceWorker,
|
||||
swPublicKey: this.swPublicKey,
|
||||
swPrivateKey: this.swPrivateKey,
|
||||
deeplAuthKey: this.deeplAuthKey,
|
||||
deeplIsPro: this.deeplIsPro,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
});
|
||||
}
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.general,
|
||||
icon: 'fas fa-cog',
|
||||
bg: 'var(--bg)',
|
||||
actions: [{
|
||||
asFullButton: true,
|
||||
icon: 'fas fa-check',
|
||||
text: i18n.ts.save,
|
||||
handler: save,
|
||||
}],
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="ieepwinx">
|
||||
<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ $ts.add }}</MkButton>
|
||||
<MkButton :link="true" to="/my/antennas/create" primary class="add"><i class="fas fa-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
|
||||
<div class="">
|
||||
<MkPagination v-slot="{items}" ref="list" :pagination="pagination">
|
||||
|
@ -14,36 +14,25 @@
|
|||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkPagination from '@/components/ui/pagination.vue';
|
||||
import MkButton from '@/components/ui/button.vue';
|
||||
import * as symbols from '@/symbols';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
MkPagination,
|
||||
MkButton,
|
||||
},
|
||||
const pagination = {
|
||||
endpoint: 'antennas/list' as const,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: this.$ts.manageAntennas,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)',
|
||||
action: {
|
||||
icon: 'fas fa-plus',
|
||||
handler: this.create
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
endpoint: 'antennas/list' as const,
|
||||
limit: 10,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
defineExpose({
|
||||
[symbols.PAGE_INFO]: {
|
||||
title: i18n.ts.manageAntennas,
|
||||
icon: 'fas fa-satellite',
|
||||
bg: 'var(--bg)'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -108,6 +108,10 @@ export default defineComponent({
|
|||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
this.hasPrev = false;
|
||||
this.hasNext = false;
|
||||
this.showPrev = false;
|
||||
this.showNext = false;
|
||||
this.note = null;
|
||||
os.api('notes/show', {
|
||||
noteId: this.noteId
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
import { defineAsyncComponent } from 'vue';
|
||||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
import { alert } from '@/os';
|
||||
import { popup } from '@/os';
|
||||
|
||||
export function pleaseLogin() {
|
||||
export function pleaseLogin(path?: string) {
|
||||
if ($i) return;
|
||||
|
||||
alert({
|
||||
title: i18n.ts.signinRequired,
|
||||
text: null
|
||||
});
|
||||
popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {
|
||||
autoSet: true,
|
||||
message: i18n.ts.signinRequired
|
||||
}, {
|
||||
cancelled: () => {
|
||||
if (path) {
|
||||
window.location.href = path;
|
||||
}
|
||||
},
|
||||
}, 'closed');
|
||||
|
||||
throw new Error('signin required');
|
||||
}
|
||||
|
|
|
@ -22,8 +22,8 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'loaded'): void;
|
||||
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(ev: 'loaded'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let timeline = $ref<InstanceType<typeof XTimeline>>();
|
||||
|
|
|
@ -29,7 +29,7 @@ defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
/*
|
||||
|
|
|
@ -61,8 +61,8 @@ const props = withDefaults(defineProps<{
|
|||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(e: 'change-active-state', v: boolean): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(ev: 'change-active-state', v: boolean): void;
|
||||
}>();
|
||||
|
||||
let body = $ref<HTMLDivElement>();
|
||||
|
@ -193,9 +193,9 @@ function goTop() {
|
|||
});
|
||||
}
|
||||
|
||||
function onDragstart(e) {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id);
|
||||
function onDragstart(ev) {
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
ev.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id);
|
||||
|
||||
// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
|
||||
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
|
||||
|
@ -204,21 +204,21 @@ function onDragstart(e) {
|
|||
}, 10);
|
||||
}
|
||||
|
||||
function onDragend(e) {
|
||||
function onDragend(ev) {
|
||||
dragging = false;
|
||||
}
|
||||
|
||||
function onDragover(e) {
|
||||
function onDragover(ev) {
|
||||
// 自分自身がドラッグされている場合
|
||||
if (dragging) {
|
||||
// 自分自身にはドロップさせない
|
||||
e.dataTransfer.dropEffect = 'none';
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const isDeckColumn = e.dataTransfer.types[0] == _DATA_TRANSFER_DECK_COLUMN_;
|
||||
const isDeckColumn = ev.dataTransfer.types[0] === _DATA_TRANSFER_DECK_COLUMN_;
|
||||
|
||||
e.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
|
||||
ev.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
|
||||
|
||||
if (!dragging && isDeckColumn) draghover = true;
|
||||
}
|
||||
|
@ -227,12 +227,12 @@ function onDragleave() {
|
|||
draghover = false;
|
||||
}
|
||||
|
||||
function onDrop(e) {
|
||||
function onDrop(ev) {
|
||||
draghover = false;
|
||||
os.deckGlobalEvents.emit('column.dragEnd');
|
||||
|
||||
const id = e.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
|
||||
if (id != null && id != '') {
|
||||
const id = ev.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
|
||||
if (id != null && id !== '') {
|
||||
swapColumn(props.column.id, id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,8 +72,8 @@ export const loadDeck = async () => {
|
|||
scope: ['client', 'deck', 'profiles'],
|
||||
key: deckStore.state.profile,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.code === 'NO_SUCH_KEY') {
|
||||
} catch (err) {
|
||||
if (err.code === 'NO_SUCH_KEY') {
|
||||
// 後方互換性のため
|
||||
if (deckStore.state.profile === 'default') {
|
||||
saveDeck();
|
||||
|
@ -94,7 +94,7 @@ export const loadDeck = async () => {
|
|||
deckStore.set('layout', [['a'], ['b']]);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
throw err;
|
||||
}
|
||||
|
||||
deckStore.set('columns', deck.columns);
|
||||
|
@ -114,7 +114,7 @@ export const saveDeck = throttle(1000, () => {
|
|||
});
|
||||
|
||||
export function addColumn(column: Column) {
|
||||
if (column.name == undefined) column.name = null;
|
||||
if (column.name === undefined) column.name = null;
|
||||
deckStore.push('columns', column);
|
||||
deckStore.push('layout', [column.id]);
|
||||
saveDeck();
|
||||
|
@ -129,10 +129,10 @@ export function removeColumn(id: Column['id']) {
|
|||
}
|
||||
|
||||
export function swapColumn(a: Column['id'], b: Column['id']) {
|
||||
const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) != -1);
|
||||
const aY = deckStore.state.layout[aX].findIndex(id => id == a);
|
||||
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) != -1);
|
||||
const bY = deckStore.state.layout[bX].findIndex(id => id == b);
|
||||
const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1);
|
||||
const aY = deckStore.state.layout[aX].findIndex(id => id === a);
|
||||
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
|
||||
const bY = deckStore.state.layout[bX].findIndex(id => id === b);
|
||||
const layout = copy(deckStore.state.layout);
|
||||
layout[aX][aY] = b;
|
||||
layout[bX][bY] = a;
|
||||
|
@ -259,7 +259,7 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
|||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = copy(deckStore.state.columns[columnIndex]);
|
||||
if (column == null) return;
|
||||
column.widgets = column.widgets.filter(w => w.id != widget.id);
|
||||
column.widgets = column.widgets.filter(w => w.id !== widget.id);
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
saveDeck();
|
||||
|
|
|
@ -18,7 +18,7 @@ defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
const pagination = {
|
||||
|
|
|
@ -22,8 +22,8 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'loaded'): void;
|
||||
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(ev: 'loaded'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let timeline = $ref<InstanceType<typeof XTimeline>>();
|
||||
|
|
|
@ -35,7 +35,7 @@ defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let pageInfo = $ref<Record<string, any> | null>(null);
|
||||
|
|
|
@ -18,7 +18,7 @@ defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
const pagination = {
|
||||
|
|
|
@ -20,7 +20,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
function func() {
|
||||
|
|
|
@ -35,8 +35,8 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'loaded'): void;
|
||||
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(ev: 'loaded'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let disabled = $ref(false);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
|
||||
<div class="wtdtxvec">
|
||||
<XWidgets v-if="column.widgets" :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
|
||||
<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
|
||||
</div>
|
||||
</XColumn>
|
||||
</template>
|
||||
|
@ -20,7 +20,7 @@ const props = defineProps<{
|
|||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
|
||||
}>();
|
||||
|
||||
let edit = $ref(false);
|
||||
|
|
|
@ -751,17 +751,10 @@ astral-regex@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
|
||||
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
|
||||
|
||||
async@^2.6.0:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
||||
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
async@^3.2.0:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8"
|
||||
integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
|
||||
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
|
@ -2519,13 +2512,6 @@ is-negative-zero@^2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
|
||||
integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
|
||||
|
||||
is-number-like@^1.0.3:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/is-number-like/-/is-number-like-1.0.8.tgz#2e129620b50891042e44e9bbbb30593e75cfbbe3"
|
||||
integrity sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==
|
||||
dependencies:
|
||||
lodash.isfinite "^3.3.2"
|
||||
|
||||
is-number-object@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0"
|
||||
|
@ -2789,11 +2775,6 @@ locate-path@^6.0.0:
|
|||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash.isfinite@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
|
||||
integrity sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=
|
||||
|
||||
lodash.merge@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
|
@ -2804,7 +2785,7 @@ lodash.once@^4.1.1:
|
|||
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
|
||||
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
|
||||
|
||||
lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21:
|
||||
lodash@^4.17.19, lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
@ -3303,14 +3284,6 @@ pngjs@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
|
||||
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
|
||||
|
||||
portscanner@2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/portscanner/-/portscanner-2.2.0.tgz#6059189b3efa0965c9d96a56b958eb9508411cf1"
|
||||
integrity sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==
|
||||
dependencies:
|
||||
async "^2.6.0"
|
||||
is-number-like "^1.0.3"
|
||||
|
||||
postcss-selector-parser@^6.0.9:
|
||||
version "6.0.9"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz#ee71c3b9ff63d9cd130838876c13a2ec1a992b2f"
|
||||
|
|
37
yarn.lock
37
yarn.lock
|
@ -382,7 +382,7 @@ arr-union@^3.1.0:
|
|||
array-each@^1.0.0, array-each@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f"
|
||||
integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8=
|
||||
integrity sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==
|
||||
|
||||
array-initial@^1.0.0:
|
||||
version "1.1.0"
|
||||
|
@ -468,9 +468,9 @@ async-settle@^1.0.0:
|
|||
async-done "^1.2.2"
|
||||
|
||||
async@^3.2.0:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8"
|
||||
integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg==
|
||||
version "3.2.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
|
||||
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
|
@ -1005,12 +1005,12 @@ copy-descriptor@^0.1.0:
|
|||
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
|
||||
|
||||
copy-props@^2.0.1:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/copy-props/-/copy-props-2.0.4.tgz#93bb1cadfafd31da5bb8a9d4b41f471ec3a72dfe"
|
||||
integrity sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/copy-props/-/copy-props-2.0.5.tgz#03cf9ae328d4ebb36f8f1d804448a6af9ee3f2d2"
|
||||
integrity sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==
|
||||
dependencies:
|
||||
each-props "^1.3.0"
|
||||
is-plain-object "^2.0.1"
|
||||
each-props "^1.3.2"
|
||||
is-plain-object "^5.0.0"
|
||||
|
||||
core-util-is@1.0.2, core-util-is@~1.0.0:
|
||||
version "1.0.2"
|
||||
|
@ -1261,7 +1261,7 @@ duplexify@^3.6.0:
|
|||
readable-stream "^2.0.0"
|
||||
stream-shift "^1.0.0"
|
||||
|
||||
each-props@^1.3.0:
|
||||
each-props@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/each-props/-/each-props-1.3.2.tgz#ea45a414d16dd5cfa419b1a81720d5ca06892333"
|
||||
integrity sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==
|
||||
|
@ -2001,9 +2001,9 @@ homedir-polyfill@^1.0.1:
|
|||
parse-passwd "^1.0.0"
|
||||
|
||||
hosted-git-info@^2.1.4:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
||||
version "2.8.9"
|
||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||
|
||||
html-comment-regex@^1.1.0:
|
||||
version "1.1.2"
|
||||
|
@ -2276,6 +2276,11 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
|||
dependencies:
|
||||
isobject "^3.0.1"
|
||||
|
||||
is-plain-object@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
|
||||
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
|
||||
|
||||
is-relative@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d"
|
||||
|
@ -2969,9 +2974,9 @@ path-key@^3.0.0, path-key@^3.1.0:
|
|||
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
|
||||
|
||||
path-parse@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
|
||||
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
path-root-regex@^0.1.0:
|
||||
version "0.1.2"
|
||||
|
|
Loading…
Reference in a new issue