Compare commits

...

17 Commits

Author SHA1 Message Date
syuilo
d0157b3bfd 13.0.0-rc.8 2023-01-15 16:55:08 +09:00
syuilo
7fc8d2e6d5 ロールでレートリミットを調整できるように
Resolve #9584
2023-01-15 16:52:12 +09:00
Masaya Suzuki
fb0f9711ba Update actions/github-script (#9588) 2023-01-15 16:14:06 +09:00
syuilo
92136272b0 enhance(client): show readable error when rate limit exceeded 2023-01-15 16:13:57 +09:00
Masaya Suzuki
e1159e9ef2 Update actions/checkout (#9587) 2023-01-15 16:03:18 +09:00
Masaya Suzuki
a2e61c6708 CI Publish Docker image (develop) をforkしたリポジトリでは実行しない (#9585) 2023-01-15 15:59:01 +09:00
Masaya Suzuki
726959911c Update actions/setup-node (#9586) 2023-01-15 15:58:10 +09:00
syuilo
d59914b959 tweak style 2023-01-15 14:18:45 +09:00
syuilo
07025caee9 refactor(client): use css modules 2023-01-15 14:03:28 +09:00
syuilo
1c0289e490 Fix #9582 2023-01-15 13:46:09 +09:00
syuilo
275fcd8bbc tweak style 2023-01-15 13:39:06 +09:00
Masaya Suzuki
0c0aa93668 GitHub Actionsとpackages/swをDependabotによるアップデート対象にする (#9572) 2023-01-15 12:12:28 +09:00
Masaya Suzuki
bfcd5ea440 Dockerで構築する場合のconfigファイルの雛形追加 (#9577) 2023-01-15 12:11:38 +09:00
Masaya Suzuki
3ff43cca02 frontendに@type/nodeをインストールする (#9571) 2023-01-15 11:55:12 +09:00
Nya Candy
6bd536c526 feat: update year in COPYING file (#9578)
Happy new year :D
2023-01-15 11:53:22 +09:00
syuilo
7738a36014 refactor(client): use css modules 2023-01-15 11:30:40 +09:00
syuilo
daddec8362 refactor(client): use css modules 2023-01-15 11:22:58 +09:00
31 changed files with 572 additions and 393 deletions

151
.config/docker_example.yml Normal file
View File

@@ -0,0 +1,151 @@
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 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 requires a reverse proxy to support HTTPS connections.
#
# +----- https://example.tld/ ------------+
# +------+ |+-------------+ +----------------+|
# | User | ---> || Proxy (443) | ---> | Misskey (3000) ||
# +------+ |+-------------+ +----------------+|
# +---------------------------------------+
#
# You need to set up a reverse proxy. (e.g. nginx)
# An encrypted connection with HTTPS is highly recommended
# because tokens may be transferred in GET requests.
# The port that your Misskey server should listen on.
port: 3000
# ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────
db:
host: db
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: redis
port: 6379
#family: 0 # 0=Both, 4=IPv4, 6=IPv6
#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:
- api.deepl.com
- api-free.deepl.com
- www.recaptcha.net
- hcaptcha.com
- challenges.cloudflare.com
# 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
# Proxy remote files (default: false)
#proxyRemoteFiles: true
# Sign to ActivityPub GET request (default: true)
signToActivityPubGet: true
#allowedPrivateNetworks: [
# '127.0.0.1/32'
#]
# Upload or download file size limits (bytes)
#maxFileSize: 262144000

View File

@@ -5,6 +5,11 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 0
- package-ecosystem: npm - package-ecosystem: npm
directory: "/" directory: "/"
schedule: schedule:
@@ -20,3 +25,8 @@ updates:
schedule: schedule:
interval: daily interval: daily
open-pull-requests-limit: 0 open-pull-requests-limit: 0
- package-ecosystem: npm
directory: "/packages/sw"
schedule:
interval: daily
open-pull-requests-limit: 0

View File

@@ -10,10 +10,10 @@ jobs:
push_to_registry: push_to_registry:
name: Push Docker image to Docker Hub name: Push Docker image to Docker Hub
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'misskey-dev/misskey'
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v2 uses: actions/checkout@v3.3.0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v3

View File

@@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v2 uses: actions/checkout@v3.3.0
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v3

View File

@@ -11,11 +11,11 @@ jobs:
yarn_install: yarn_install:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3.3.0
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: actions/setup-node@v3.2.0 - uses: actions/setup-node@v3.6.0
with: with:
node-version: 18.x node-version: 18.x
cache: 'yarn' cache: 'yarn'
@@ -33,11 +33,11 @@ jobs:
- frontend - frontend
- sw - sw
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3.3.0
with: with:
fetch-depth: 0 fetch-depth: 0
submodules: true submodules: true
- uses: actions/setup-node@v3.2.0 - uses: actions/setup-node@v3.6.0
with: with:
node-version: 18.x node-version: 18.x
cache: 'yarn' cache: 'yarn'

View File

@@ -13,7 +13,7 @@ jobs:
github.event.client_payload.slash_command.sha != '' && github.event.client_payload.slash_command.sha != '' &&
contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha) contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha)
steps: steps:
- uses: actions/github-script@v5 - uses: actions/github-script@v6.3.3
id: check-id id: check-id
env: env:
number: ${{ github.event.client_payload.pull_request.number }} number: ${{ github.event.client_payload.pull_request.number }}
@@ -37,7 +37,7 @@ jobs:
return check[0].id; return check[0].id;
- uses: actions/github-script@v5 - uses: actions/github-script@v6.3.3
env: env:
check_id: ${{ steps.check-id.outputs.result }} check_id: ${{ steps.check-id.outputs.result }}
details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }} details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }}
@@ -53,7 +53,7 @@ jobs:
# Check out merge commit # Check out merge commit
- name: Fork based /deploy checkout - name: Fork based /deploy checkout
uses: actions/checkout@v2 uses: actions/checkout@v3.3.0
with: with:
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge' ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
@@ -72,7 +72,7 @@ jobs:
timeout: 15m timeout: 15m
# Update check run called "integration-fork" # Update check run called "integration-fork"
- uses: actions/github-script@v5 - uses: actions/github-script@v6.3.3
id: update-check-run id: update-check-run
if: ${{ always() }} if: ${{ always() }}
env: env:

View File

@@ -30,11 +30,11 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3.3.0
with: with:
submodules: true submodules: true
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.2.0 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'yarn' cache: 'yarn'
@@ -77,7 +77,7 @@ jobs:
- 56312:6379 - 56312:6379
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3.3.0
with: with:
submodules: true submodules: true
# https://github.com/cypress-io/cypress-docker-images/issues/150 # https://github.com/cypress-io/cypress-docker-images/issues/150
@@ -87,7 +87,7 @@ jobs:
#- uses: browser-actions/setup-firefox@latest #- uses: browser-actions/setup-firefox@latest
# if: ${{ matrix.browser == 'firefox' }} # if: ${{ matrix.browser == 'firefox' }}
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3.2.0 uses: actions/setup-node@v3.6.0
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'yarn' cache: 'yarn'

1
.gitignore vendored
View File

@@ -30,6 +30,7 @@ coverage
# config # config
/.config/* /.config/*
!/.config/example.yml !/.config/example.yml
!/.config/docker_example.yml
!/.config/docker_example.env !/.config/docker_example.env
# misskey # misskey

View File

@@ -74,6 +74,7 @@ You should also include the user name that made the change.
- Push notification of Antenna note @tamaina - Push notification of Antenna note @tamaina
- AVIF support @tamaina - AVIF support @tamaina
- Add Cloudflare Turnstile CAPTCHA support @CyberRex0 - Add Cloudflare Turnstile CAPTCHA support @CyberRex0
- レートリミットをユーザーごとに調整可能に @syuilo
- 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo - 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo
- 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo - 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo
- クリップおよびクリップ内のノートの作成可能数を設定可能に @syuilo - クリップおよびクリップ内のノートの作成可能数を設定可能に @syuilo
@@ -99,6 +100,7 @@ You should also include the user name that made the change.
- Client: Add link to user RSS feed in profile menu @ssmucny - Client: Add link to user RSS feed in profile menu @ssmucny
- Client: Compress non-animated PNG files @saschanaz - Client: Compress non-animated PNG files @saschanaz
- Client: YouTube window player @sim1222 - Client: YouTube window player @sim1222
- Client: show readable error when rate limit exceeded @syuilo
- Client: enhance dashboard of control panel @syuilo - Client: enhance dashboard of control panel @syuilo
- Client: Vite is upgraded to v4 @syuilo, @tamaina - Client: Vite is upgraded to v4 @syuilo, @tamaina
- Client: HMR is available while yarn dev @tamaina - Client: HMR is available while yarn dev @tamaina

View File

@@ -1,5 +1,5 @@
Unless otherwise stated this repository is Unless otherwise stated this repository is
Copyright © 2014-2022 syuilo and contributers Copyright © 2014-2023 syuilo and contributers
And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE. And is distributed under The GNU Affero General Public License Version 3, you should have received a copy of the license file as LICENSE.

View File

@@ -933,6 +933,8 @@ unassign: "アサインを解除"
color: "色" color: "色"
manageCustomEmojis: "カスタム絵文字の管理" manageCustomEmojis: "カスタム絵文字の管理"
youCannotCreateAnymore: "これ以上作成することはできません。" youCannotCreateAnymore: "これ以上作成することはできません。"
cannotPerformTemporary: "一時的に利用できません"
cannotPerformTemporaryDescription: "操作回数が制限を超過するため一時的に利用できません。しばらく時間を置いてから再度お試しください。"
_role: _role:
new: "ロールの作成" new: "ロールの作成"
@@ -970,6 +972,8 @@ _role:
noteEachClipsMax: "クリップ内のノートの最大数" noteEachClipsMax: "クリップ内のノートの最大数"
userListMax: "ユーザーリストの作成可能数" userListMax: "ユーザーリストの作成可能数"
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数" userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
_condition: _condition:
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"
isRemote: "リモートユーザー" isRemote: "リモートユーザー"

View File

@@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.0.0-rc.7", "version": "13.0.0-rc.8",
"codename": "indigo", "codename": "indigo",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -28,6 +28,7 @@ export type RoleOptions = {
noteEachClipsLimit: number; noteEachClipsLimit: number;
userListLimit: number; userListLimit: number;
userEachUserListsLimit: number; userEachUserListsLimit: number;
rateLimitFactor: number;
}; };
export const DEFAULT_ROLE: RoleOptions = { export const DEFAULT_ROLE: RoleOptions = {
@@ -45,6 +46,7 @@ export const DEFAULT_ROLE: RoleOptions = {
noteEachClipsLimit: 200, noteEachClipsLimit: 200,
userListLimit: 10, userListLimit: 10,
userEachUserListsLimit: 50, userEachUserListsLimit: 50,
rateLimitFactor: 1,
}; };
@Injectable() @Injectable()
@@ -221,6 +223,7 @@ export class RoleService implements OnApplicationShutdown {
noteEachClipsLimit: Math.max(...getOptionValues('noteEachClipsLimit')), noteEachClipsLimit: Math.max(...getOptionValues('noteEachClipsLimit')),
userListLimit: Math.max(...getOptionValues('userListLimit')), userListLimit: Math.max(...getOptionValues('userListLimit')),
userEachUserListsLimit: Math.max(...getOptionValues('userEachUserListsLimit')), userEachUserListsLimit: Math.max(...getOptionValues('userEachUserListsLimit')),
rateLimitFactor: Math.max(...getOptionValues('rateLimitFactor')),
}; };
} }

View File

@@ -224,8 +224,11 @@ export class ApiCallService implements OnApplicationShutdown {
limit.key = ep.name; limit.key = ep.name;
} }
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
const factor = user ? (await this.roleService.getUserRoleOptions(user.id)).rateLimitFactor : 1;
// Rate limit // Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(err => { await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
throw new ApiError({ throw new ApiError({
message: 'Rate limit exceeded. Please try again later.', message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED', code: 'RATE_LIMIT_EXCEEDED',

View File

@@ -26,7 +26,7 @@ export class RateLimiterService {
} }
@bindThis @bindThis
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) { public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
return new Promise<void>((ok, reject) => { return new Promise<void>((ok, reject) => {
if (this.disabled) ok(); if (this.disabled) ok();
@@ -34,7 +34,7 @@ export class RateLimiterService {
const min = (): void => { const min = (): void => {
const minIntervalLimiter = new Limiter({ const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`, id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval, duration: limitation.minInterval * factor,
max: 1, max: 1,
db: this.redisClient, db: this.redisClient,
}); });
@@ -62,8 +62,8 @@ export class RateLimiterService {
const max = (): void => { const max = (): void => {
const limiter = new Limiter({ const limiter = new Limiter({
id: `${actor}:${limitation.key}`, id: `${actor}:${limitation.key}`,
duration: limitation.duration, duration: limitation.duration * factor,
max: limitation.max, max: limitation.max / factor,
db: this.redisClient, db: this.redisClient,
}); });

View File

@@ -73,6 +73,7 @@
"@types/gulp": "4.0.10", "@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1", "@types/gulp-rename": "2.0.1",
"@types/matter-js": "0.18.2", "@types/matter-js": "0.18.2",
"@types/node": "^18.11.18",
"@types/punycode": "2.1.0", "@types/punycode": "2.1.0",
"@types/sanitize-html": "^2.8.0", "@types/sanitize-html": "^2.8.0",
"@types/seedrandom": "3.0.4", "@types/seedrandom": "3.0.4",

View File

@@ -78,9 +78,9 @@ const inputEl = shallowRef<HTMLElement>();
const prefixEl = shallowRef<HTMLElement>(); const prefixEl = shallowRef<HTMLElement>();
const suffixEl = shallowRef<HTMLElement>(); const suffixEl = shallowRef<HTMLElement>();
const height = const height =
props.small ? 34 : props.small ? 33 :
props.large ? 40 : props.large ? 39 :
37; 36;
const focus = () => inputEl.value.focus(); const focus = () => inputEl.value.focus();
const onInput = (ev: KeyboardEvent) => { const onInput = (ev: KeyboardEvent) => {

View File

@@ -1,12 +1,12 @@
<template> <template>
<div class="fefdfafb"> <div :class="$style.root">
<MkAvatar class="avatar" :user="$i"/> <MkAvatar :class="$style.avatar" :user="$i"/>
<div class="main"> <div :class="$style.main">
<div class="header"> <div :class="$style.header">
<MkUserName :user="$i"/> <MkUserName :user="$i"/>
</div> </div>
<div class="body"> <div>
<div class="content"> <div :class="$style.content">
<Mfm :text="text.trim()" :author="$i" :i="$i"/> <Mfm :text="text.trim()" :author="$i" :i="$i"/>
</div> </div>
</div> </div>
@@ -22,75 +22,48 @@ const props = defineProps<{
}>(); }>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.fefdfafb { .root {
display: flex; display: flex;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: clip; overflow: clip;
font-size: 0.95em; font-size: 0.95em;
}
> .avatar { .avatar {
flex-shrink: 0; flex-shrink: 0 !important;
display: block; display: block !important;
margin: 0 10px 0 0; margin: 0 10px 0 0 !important;
width: 40px; width: 40px !important;
height: 40px; height: 40px !important;
border-radius: 8px; border-radius: 8px !important;
pointer-events: none; pointer-events: none !important;
} }
> .main { .main {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
}
> .header { .header {
margin-bottom: 2px; margin-bottom: 2px;
font-weight: bold; font-weight: bold;
}
> .body {
> .cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
> .text {
margin-right: 8px;
}
}
> .content {
> .text {
cursor: default;
margin: 0;
padding: 0;
}
}
}
}
} }
@container (min-width: 350px) { @container (min-width: 350px) {
.fefdfafb { .avatar {
> .avatar { margin: 0 10px 0 0 !important;
margin: 0 10px 0 0; width: 44px !important;
width: 44px; height: 44px !important;
height: 44px;
}
} }
} }
@container (min-width: 500px) { @container (min-width: 500px) {
.fefdfafb { .avatar {
> .avatar { margin: 0 12px 0 0 !important;
margin: 0 12px 0 0; width: 48px !important;
width: 48px; height: 48px !important;
height: 48px;
}
} }
} }
</style> </style>

View File

@@ -173,7 +173,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
$thumbWidth: 20px; $thumbWidth: 20px;
> .body { > .body {
padding: 8px 12px; padding: 7px 12px;
background: var(--panel); background: var(--panel);
border: solid 1px var(--panel); border: solid 1px var(--panel);
border-radius: 6px; border-radius: 6px;

View File

@@ -65,9 +65,9 @@ const prefixEl = ref(null);
const suffixEl = ref(null); const suffixEl = ref(null);
const container = ref(null); const container = ref(null);
const height = const height =
props.small ? 34 : props.small ? 33 :
props.large ? 40 : props.large ? 39 :
37; 36;
const focus = () => inputEl.value.focus(); const focus = () => inputEl.value.focus();
const onInput = (ev) => { const onInput = (ev) => {

View File

@@ -1,42 +1,42 @@
<template> <template>
<MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')"> <MkModal ref="modal" :z-priority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
<div class="gqyayizv _popup"> <div class="_popup" :class="$style.root">
<button key="public" class="_button item" :class="{ active: v === 'public' }" data-index="1" @click="choose('public')"> <button key="public" class="_button" :class="[$style.item, { [$style.active]: v === 'public' }]" data-index="1" @click="choose('public')">
<div class="icon"><i class="ti ti-world"></i></div> <div :class="$style.icon"><i class="ti ti-world"></i></div>
<div class="body"> <div :class="$style.body">
<span>{{ i18n.ts._visibility.public }}</span> <span :class="$style.itemTitle">{{ i18n.ts._visibility.public }}</span>
<span>{{ i18n.ts._visibility.publicDescription }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.publicDescription }}</span>
</div> </div>
</button> </button>
<button key="home" class="_button item" :class="{ active: v === 'home' }" data-index="2" @click="choose('home')"> <button key="home" class="_button" :class="[$style.item, { [$style.active]: v === 'home' }]" data-index="2" @click="choose('home')">
<div class="icon"><i class="ti ti-home"></i></div> <div :class="$style.icon"><i class="ti ti-home"></i></div>
<div class="body"> <div :class="$style.body">
<span>{{ i18n.ts._visibility.home }}</span> <span :class="$style.itemTitle">{{ i18n.ts._visibility.home }}</span>
<span>{{ i18n.ts._visibility.homeDescription }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.homeDescription }}</span>
</div> </div>
</button> </button>
<button key="followers" class="_button item" :class="{ active: v === 'followers' }" data-index="3" @click="choose('followers')"> <button key="followers" class="_button" :class="[$style.item, { [$style.active]: v === 'followers' }]" data-index="3" @click="choose('followers')">
<div class="icon"><i class="ti ti-lock"></i></div> <div :class="$style.icon"><i class="ti ti-lock"></i></div>
<div class="body"> <div :class="$style.body">
<span>{{ i18n.ts._visibility.followers }}</span> <span :class="$style.itemTitle">{{ i18n.ts._visibility.followers }}</span>
<span>{{ i18n.ts._visibility.followersDescription }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.followersDescription }}</span>
</div> </div>
</button> </button>
<button key="specified" :disabled="localOnly" class="_button item" :class="{ active: v === 'specified' }" data-index="4" @click="choose('specified')"> <button key="specified" :disabled="localOnly" class="_button" :class="[$style.item, { [$style.active]: v === 'specified' }]" data-index="4" @click="choose('specified')">
<div class="icon"><i class="ti ti-mail"></i></div> <div :class="$style.icon"><i class="ti ti-mail"></i></div>
<div class="body"> <div :class="$style.body">
<span>{{ i18n.ts._visibility.specified }}</span> <span :class="$style.itemTitle">{{ i18n.ts._visibility.specified }}</span>
<span>{{ i18n.ts._visibility.specifiedDescription }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.specifiedDescription }}</span>
</div> </div>
</button> </button>
<div class="divider"></div> <div :class="$style.divider"></div>
<button key="localOnly" class="_button item localOnly" :class="{ active: localOnly }" data-index="5" @click="localOnly = !localOnly"> <button key="localOnly" class="_button" :class="[$style.item, $style.localOnly, { [$style.active]: localOnly }]" data-index="5" @click="localOnly = !localOnly">
<div class="icon"><i class="ti ti-world-off"></i></div> <div :class="$style.icon"><i class="ti ti-world-off"></i></div>
<div class="body"> <div :class="$style.body">
<span>{{ i18n.ts._visibility.localOnly }}</span> <span :class="$style.itemTitle">{{ i18n.ts._visibility.localOnly }}</span>
<span>{{ i18n.ts._visibility.localOnlyDescription }}</span> <span :class="$style.itemDescription">{{ i18n.ts._visibility.localOnlyDescription }}</span>
</div> </div>
<div class="toggle"><i :class="localOnly ? 'ti ti-toggle-right' : 'ti ti-toggle-left'"></i></div> <div :class="$style.toggle"><i :class="localOnly ? 'ti ti-toggle-right' : 'ti ti-toggle-left'"></i></div>
</button> </button>
</div> </div>
</MkModal> </MkModal>
@@ -79,81 +79,81 @@ function choose(visibility: typeof misskey.noteVisibilities[number]): void {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.gqyayizv { .root {
width: 240px; width: 240px;
padding: 8px 0; padding: 8px 0;
}
> .divider { .divider {
margin: 8px 0; margin: 8px 0;
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
}
.item {
display: flex;
padding: 8px 14px;
font-size: 12px;
text-align: left;
width: 100%;
box-sizing: border-box;
&:hover {
background: rgba(0, 0, 0, 0.05);
} }
> .item { &:active {
display: flex; background: rgba(0, 0, 0, 0.1);
padding: 8px 14px; }
font-size: 12px;
text-align: left;
width: 100%;
box-sizing: border-box;
&:hover { &.active {
background: rgba(0, 0, 0, 0.05); color: var(--fgOnAccent);
} background: var(--accent);
}
&:active { &.localOnly.active {
background: rgba(0, 0, 0, 0.1); color: var(--accent);
} background: inherit;
&.active {
color: var(--fgOnAccent);
background: var(--accent);
}
&.localOnly.active {
color: var(--accent);
background: inherit;
}
> .icon {
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
width: 16px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
}
> .body {
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
> span:first-child {
display: block;
font-weight: bold;
}
> span:last-child:not(:first-child) {
opacity: 0.6;
}
}
> .toggle {
display: flex;
justify-content: center;
align-items: center;
margin-left: 10px;
width: 16px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
}
} }
} }
.icon {
display: flex;
justify-content: center;
align-items: center;
margin-right: 10px;
width: 16px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
}
.body {
flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.itemTitle {
display: block;
font-weight: bold;
}
.itemDescription {
opacity: 0.6;
}
.toggle {
display: flex;
justify-content: center;
align-items: center;
margin-left: 10px;
width: 16px;
top: 0;
bottom: 0;
margin-top: auto;
margin-bottom: auto;
}
</style> </style>

View File

@@ -1,9 +1,9 @@
<template> <template>
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')"> <MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="success ? done() : () => {}" @closed="emit('closed')">
<div class="iuyakobc" :class="{ iconOnly: (text == null) || success }"> <div :class="[$style.root, { [$style.iconOnly]: (text == null) || success }]">
<i v-if="success" class="ti ti-check icon success"></i> <i v-if="success" :class="[$style.icon, $style.success]" class="ti ti-check"></i>
<MkLoading v-else class="icon waiting" :em="true"/> <MkLoading v-else :class="[$style.icon, $style.waiting]" :em="true"/>
<div v-if="text && !success" class="text">{{ text }}<MkEllipsis/></div> <div v-if="text && !success" :class="$style.text">{{ text }}<MkEllipsis/></div>
</div> </div>
</MkModal> </MkModal>
</template> </template>
@@ -35,8 +35,8 @@ watch(() => props.showing, () => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.iuyakobc { .root {
margin: auto; margin: auto;
position: relative; position: relative;
padding: 32px; padding: 32px;
@@ -54,21 +54,21 @@ watch(() => props.showing, () => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
}
> .icon { .icon {
font-size: 32px; font-size: 32px;
&.success { &.success {
color: var(--accent); color: var(--accent);
}
&.waiting {
opacity: 0.7;
}
} }
> .text { &.waiting {
margin-top: 16px; opacity: 0.7;
} }
} }
.text {
margin-top: 16px;
}
</style> </style>

View File

@@ -20,7 +20,10 @@ export const apiWithDialog = ((
promiseDialog(promise, null, (err) => { promiseDialog(promise, null, (err) => {
let title = null; let title = null;
let text = err.message + '\n' + (err as any).id; let text = err.message + '\n' + (err as any).id;
if (err.code.startsWith('TOO_MANY')) { if (err.code === 'RATE_LIMIT_EXCEEDED') {
title = i18n.ts.cannotPerformTemporary;
text = i18n.ts.cannotPerformTemporaryDescription;
} else if (err.code.startsWith('TOO_MANY')) {
title = i18n.ts.youCannotCreateAnymore; title = i18n.ts.youCannotCreateAnymore;
text = `${i18n.ts.error}: ${err.id}`; text = `${i18n.ts.error}: ${err.id}`;
} }

View File

@@ -38,6 +38,19 @@
<FormSlot> <FormSlot>
<template #label>{{ i18n.ts._role.options }}</template> <template #label>{{ i18n.ts._role.options }}</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkFolder>
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
<template #suffix>{{ options_rateLimitFactor_useDefault ? i18n.ts._role.useBaseValue : `${Math.floor(options_rateLimitFactor_value * 100)}%` }}</template>
<div class="_gaps">
<MkSwitch v-model="options_rateLimitFactor_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkRange :model-value="options_rateLimitFactor_value * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => options_rateLimitFactor_value = (v / 100)">
<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template> <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
<template #suffix>{{ options_gtlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_gtlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template> <template #suffix>{{ options_gtlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_gtlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template>
@@ -241,9 +254,11 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
import FormSlot from '@/components/form/slot.vue'; import FormSlot from '@/components/form/slot.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { instance } from '@/instance';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'created', payload: any): void; (ev: 'created', payload: any): void;
@@ -266,33 +281,35 @@ let condFormula = $ref(role?.condFormula ?? { id: uuid(), type: 'isRemote' });
let isPublic = $ref(role?.isPublic ?? false); let isPublic = $ref(role?.isPublic ?? false);
let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false); let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false);
let options_gtlAvailable_useDefault = $ref(role?.options?.gtlAvailable?.useDefault ?? true); let options_gtlAvailable_useDefault = $ref(role?.options?.gtlAvailable?.useDefault ?? true);
let options_gtlAvailable_value = $ref(role?.options?.gtlAvailable?.value ?? false); let options_gtlAvailable_value = $ref(role?.options?.gtlAvailable?.value ?? instance.baseRole.gtlAvailable);
let options_ltlAvailable_useDefault = $ref(role?.options?.ltlAvailable?.useDefault ?? true); let options_ltlAvailable_useDefault = $ref(role?.options?.ltlAvailable?.useDefault ?? true);
let options_ltlAvailable_value = $ref(role?.options?.ltlAvailable?.value ?? false); let options_ltlAvailable_value = $ref(role?.options?.ltlAvailable?.value ?? instance.baseRole.ltlAvailable);
let options_canPublicNote_useDefault = $ref(role?.options?.canPublicNote?.useDefault ?? true); let options_canPublicNote_useDefault = $ref(role?.options?.canPublicNote?.useDefault ?? true);
let options_canPublicNote_value = $ref(role?.options?.canPublicNote?.value ?? false); let options_canPublicNote_value = $ref(role?.options?.canPublicNote?.value ?? instance.baseRole.canPublicNote);
let options_canInvite_useDefault = $ref(role?.options?.canInvite?.useDefault ?? true); let options_canInvite_useDefault = $ref(role?.options?.canInvite?.useDefault ?? true);
let options_canInvite_value = $ref(role?.options?.canInvite?.value ?? false); let options_canInvite_value = $ref(role?.options?.canInvite?.value ?? instance.baseRole.canInvite);
let options_canManageCustomEmojis_useDefault = $ref(role?.options?.canManageCustomEmojis?.useDefault ?? true); let options_canManageCustomEmojis_useDefault = $ref(role?.options?.canManageCustomEmojis?.useDefault ?? true);
let options_canManageCustomEmojis_value = $ref(role?.options?.canManageCustomEmojis?.value ?? false); let options_canManageCustomEmojis_value = $ref(role?.options?.canManageCustomEmojis?.value ?? instance.baseRole.canManageCustomEmojis);
let options_driveCapacityMb_useDefault = $ref(role?.options?.driveCapacityMb?.useDefault ?? true); let options_driveCapacityMb_useDefault = $ref(role?.options?.driveCapacityMb?.useDefault ?? true);
let options_driveCapacityMb_value = $ref(role?.options?.driveCapacityMb?.value ?? 0); let options_driveCapacityMb_value = $ref(role?.options?.driveCapacityMb?.value ?? instance.baseRole.driveCapacityMb);
let options_pinLimit_useDefault = $ref(role?.options?.pinLimit?.useDefault ?? true); let options_pinLimit_useDefault = $ref(role?.options?.pinLimit?.useDefault ?? true);
let options_pinLimit_value = $ref(role?.options?.pinLimit?.value ?? 0); let options_pinLimit_value = $ref(role?.options?.pinLimit?.value ?? instance.baseRole.pinLimit);
let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefault ?? true); let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefault ?? true);
let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? 0); let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? instance.baseRole.antennaLimit);
let options_wordMuteLimit_useDefault = $ref(role?.options?.wordMuteLimit?.useDefault ?? true); let options_wordMuteLimit_useDefault = $ref(role?.options?.wordMuteLimit?.useDefault ?? true);
let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? 0); let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? instance.baseRole.wordMuteLimit);
let options_webhookLimit_useDefault = $ref(role?.options?.webhookLimit?.useDefault ?? true); let options_webhookLimit_useDefault = $ref(role?.options?.webhookLimit?.useDefault ?? true);
let options_webhookLimit_value = $ref(role?.options?.webhookLimit?.value ?? 0); let options_webhookLimit_value = $ref(role?.options?.webhookLimit?.value ?? instance.baseRole.webhookLimit);
let options_clipLimit_useDefault = $ref(role?.options?.clipLimit?.useDefault ?? true); let options_clipLimit_useDefault = $ref(role?.options?.clipLimit?.useDefault ?? true);
let options_clipLimit_value = $ref(role?.options?.clipLimit?.value ?? 0); let options_clipLimit_value = $ref(role?.options?.clipLimit?.value ?? instance.baseRole.clipLimit);
let options_noteEachClipsLimit_useDefault = $ref(role?.options?.noteEachClipsLimit?.useDefault ?? true); let options_noteEachClipsLimit_useDefault = $ref(role?.options?.noteEachClipsLimit?.useDefault ?? true);
let options_noteEachClipsLimit_value = $ref(role?.options?.noteEachClipsLimit?.value ?? 0); let options_noteEachClipsLimit_value = $ref(role?.options?.noteEachClipsLimit?.value ?? instance.baseRole.noteEachClipsLimit);
let options_userListLimit_useDefault = $ref(role?.options?.userListLimit?.useDefault ?? true); let options_userListLimit_useDefault = $ref(role?.options?.userListLimit?.useDefault ?? true);
let options_userListLimit_value = $ref(role?.options?.userListLimit?.value ?? 0); let options_userListLimit_value = $ref(role?.options?.userListLimit?.value ?? instance.baseRole.userListLimit);
let options_userEachUserListsLimit_useDefault = $ref(role?.options?.userEachUserListsLimit?.useDefault ?? true); let options_userEachUserListsLimit_useDefault = $ref(role?.options?.userEachUserListsLimit?.useDefault ?? true);
let options_userEachUserListsLimit_value = $ref(role?.options?.userEachUserListsLimit?.value ?? 0); let options_userEachUserListsLimit_value = $ref(role?.options?.userEachUserListsLimit?.value ?? instance.baseRole.userEachUserListsLimit);
let options_rateLimitFactor_useDefault = $ref(role?.options?.rateLimitFactor?.useDefault ?? true);
let options_rateLimitFactor_value = $ref(role?.options?.rateLimitFactor?.value ?? instance.baseRole.rateLimitFactor);
if (_DEV_) { if (_DEV_) {
watch($$(condFormula), () => { watch($$(condFormula), () => {
@@ -316,6 +333,7 @@ function getOptions() {
noteEachClipsLimit: { useDefault: options_noteEachClipsLimit_useDefault, value: options_noteEachClipsLimit_value }, noteEachClipsLimit: { useDefault: options_noteEachClipsLimit_useDefault, value: options_noteEachClipsLimit_value },
userListLimit: { useDefault: options_userListLimit_useDefault, value: options_userListLimit_value }, userListLimit: { useDefault: options_userListLimit_useDefault, value: options_userListLimit_value },
userEachUserListsLimit: { useDefault: options_userEachUserListsLimit_useDefault, value: options_userEachUserListsLimit_value }, userEachUserListsLimit: { useDefault: options_userEachUserListsLimit_useDefault, value: options_userEachUserListsLimit_value },
rateLimitFactor: { useDefault: options_rateLimitFactor_useDefault, value: options_rateLimitFactor_value },
}; };
} }

View File

@@ -8,6 +8,14 @@
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role.baseRole }}</template> <template #label>{{ i18n.ts._role.baseRole }}</template>
<div class="_gaps"> <div class="_gaps">
<MkFolder>
<template #label>{{ i18n.ts._role._options.rateLimitFactor }}</template>
<template #suffix>{{ Math.floor(options_rateLimitFactor * 100) }}%</template>
<MkRange :model-value="options_rateLimitFactor * 100" :min="30" :max="300" :step="10" :text-converter="(v) => `${v}%`" @update:model-value="v => options_rateLimitFactor = (v / 100)">
<template #caption>{{ i18n.ts._role._options.descriptionOfRateLimitFactor }}</template>
</MkRange>
</MkFolder>
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts._role._options.gtlAvailable }}</template> <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
<template #suffix>{{ options_gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template> <template #suffix>{{ options_gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
@@ -134,6 +142,7 @@ import MkPagination from '@/components/MkPagination.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkRange from '@/components/MkRange.vue';
import MkRolePreview from '@/components/MkRolePreview.vue'; import MkRolePreview from '@/components/MkRolePreview.vue';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
@@ -159,6 +168,7 @@ let options_clipLimit = $ref(instance.baseRole.clipLimit);
let options_noteEachClipsLimit = $ref(instance.baseRole.noteEachClipsLimit); let options_noteEachClipsLimit = $ref(instance.baseRole.noteEachClipsLimit);
let options_userListLimit = $ref(instance.baseRole.userListLimit); let options_userListLimit = $ref(instance.baseRole.userListLimit);
let options_userEachUserListsLimit = $ref(instance.baseRole.userEachUserListsLimit); let options_userEachUserListsLimit = $ref(instance.baseRole.userEachUserListsLimit);
let options_rateLimitFactor = $ref(instance.baseRole.rateLimitFactor);
async function updateBaseRole() { async function updateBaseRole() {
await os.apiWithDialog('admin/roles/update-default-role-override', { await os.apiWithDialog('admin/roles/update-default-role-override', {
@@ -177,6 +187,7 @@ async function updateBaseRole() {
noteEachClipsLimit: options_noteEachClipsLimit, noteEachClipsLimit: options_noteEachClipsLimit,
userListLimit: options_userListLimit, userListLimit: options_userListLimit,
userEachUserListsLimit: options_userEachUserListsLimit, userEachUserListsLimit: options_userEachUserListsLimit,
rateLimitFactor: options_rateLimitFactor,
}, },
}); });
} }

View File

@@ -17,7 +17,7 @@
} }
::selection { ::selection {
color: #fff; color: var(--fgOnAccent);
background-color: var(--accent); background-color: var(--accent);
} }
@@ -150,10 +150,8 @@ hr {
} }
._ghost { ._ghost {
&, * { @extend ._noSelect;
@extend ._noSelect; pointer-events: none;
pointer-events: none;
}
} }
._modalBg { ._modalBg {

View File

@@ -1,9 +1,9 @@
<template> <template>
<div v-if="hasDisconnected && $store.state.serverDisconnectedBehavior === 'quiet'" class="nsbbhtug" @click="resetDisconnected"> <div v-if="hasDisconnected && $store.state.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected">
<div>{{ i18n.ts.disconnectedFromServer }}</div> <div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.disconnectedFromServer }}</div>
<div class="command"> <div :class="$style.command" class="_buttons">
<button class="_textButton" @click="reload">{{ i18n.ts.reload }}</button> <MkButton :class="$style.commandButton" small primary @click="reload">{{ i18n.ts.reload }}</MkButton>
<button class="_textButton">{{ i18n.ts.doNothing }}</button> <MkButton :class="$style.commandButton" small>{{ i18n.ts.doNothing }}</MkButton>
</div> </div>
</div> </div>
</template> </template>
@@ -12,6 +12,10 @@
import { onUnmounted } from 'vue'; import { onUnmounted } from 'vue';
import { stream } from '@/stream'; import { stream } from '@/stream';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os';
const zIndex = os.claimZIndex('high');
let hasDisconnected = $ref(false); let hasDisconnected = $ref(false);
@@ -34,28 +38,22 @@ onUnmounted(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.nsbbhtug { .root {
position: fixed; position: fixed;
z-index: 16385; z-index: v-bind(zIndex);
bottom: calc(var(--minBottomSpacing) + var(--margin)); bottom: calc(var(--minBottomSpacing) + var(--margin));
right: var(--margin); right: var(--margin);
margin: 0; margin: 0;
padding: 6px 12px; padding: 12px;
font-size: 0.9em; font-size: 0.9em;
color: #fff;
background: #000;
opacity: 0.8;
border-radius: 4px;
max-width: 320px; max-width: 320px;
}
> .command { .command {
display: flex; margin-top: 8px;
justify-content: space-around; }
> button { .commandButton {
padding: 0.7em;
}
}
} }
</style> </style>

View File

@@ -1,31 +1,31 @@
<template> <template>
<div class="mkw-calendar" :class="{ _panel: !widgetProps.transparent }"> <div :class="[$style.root, { _panel: !widgetProps.transparent }]">
<div class="calendar" :class="{ isHoliday }"> <div :class="[$style.calendar, { [$style.isHoliday]: isHoliday }]">
<p class="month-and-year"> <p :class="$style.monthAndYear">
<span class="year">{{ $t('yearX', { year }) }}</span> <span :class="$style.year">{{ $t('yearX', { year }) }}</span>
<span class="month">{{ $t('monthX', { month }) }}</span> <span :class="$style.month">{{ $t('monthX', { month }) }}</span>
</p> </p>
<p v-if="month === 1 && day === 1" class="day">🎉{{ $t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p> <p v-if="month === 1 && day === 1" class="day">🎉{{ $t('dayX', { day }) }}<span style="display: inline-block; transform: scaleX(-1);">🎉</span></p>
<p v-else class="day">{{ $t('dayX', { day }) }}</p> <p v-else :class="$style.day">{{ $t('dayX', { day }) }}</p>
<p class="week-day">{{ weekDay }}</p> <p :class="$style.weekDay">{{ weekDay }}</p>
</div> </div>
<div class="info"> <div :class="$style.info">
<div> <div :class="$style.infoSection">
<p>{{ i18n.ts.today }}<b>{{ dayP.toFixed(1) }}%</b></p> <p :class="$style.infoText">{{ i18n.ts.today }}<b :class="$style.percentage">{{ dayP.toFixed(1) }}%</b></p>
<div class="meter"> <div :class="$style.meter">
<div class="val" :style="{ width: `${dayP}%` }"></div> <div :class="$style.meterVal" :style="{ width: `${dayP}%` }"></div>
</div> </div>
</div> </div>
<div> <div :class="$style.infoSection">
<p>{{ i18n.ts.thisMonth }}<b>{{ monthP.toFixed(1) }}%</b></p> <p :class="$style.infoText">{{ i18n.ts.thisMonth }}<b :class="$style.percentage">{{ monthP.toFixed(1) }}%</b></p>
<div class="meter"> <div :class="$style.meter">
<div class="val" :style="{ width: `${monthP}%` }"></div> <div :class="$style.meterVal" :style="{ width: `${monthP}%` }"></div>
</div> </div>
</div> </div>
<div> <div :class="$style.infoSection">
<p>{{ i18n.ts.thisYear }}<b>{{ yearP.toFixed(1) }}%</b></p> <p :class="$style.infoText">{{ i18n.ts.thisYear }}<b :class="$style.percentage">{{ yearP.toFixed(1) }}%</b></p>
<div class="meter"> <div :class="$style.meter">
<div class="val" :style="{ width: `${yearP}%` }"></div> <div :class="$style.meterVal" :style="{ width: `${yearP}%` }"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -115,8 +115,8 @@ defineExpose<WidgetComponentExpose>({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.mkw-calendar { .root {
padding: 16px 0; padding: 16px 0;
&:after { &:after {
@@ -124,91 +124,93 @@ defineExpose<WidgetComponentExpose>({
display: block; display: block;
clear: both; clear: both;
} }
}
> .calendar { .calendar {
float: left; float: left;
width: 60%; width: 60%;
text-align: center; text-align: center;
&.isHoliday {
> .day {
color: #ef95a0;
}
}
> .month-and-year, > .week-day {
margin: 0;
line-height: 18px;
font-size: 0.9em;
> .year, > .month {
margin: 0 4px;
}
}
&.isHoliday {
> .day { > .day {
margin: 10px 0; color: #ef95a0;
line-height: 32px;
font-size: 1.75em;
}
}
> .info {
display: block;
float: left;
width: 40%;
padding: 0 16px 0 0;
box-sizing: border-box;
> div {
margin-bottom: 8px;
&:last-child {
margin-bottom: 4px;
}
> p {
display: flex;
margin: 0 0 2px 0;
font-size: 0.75em;
line-height: 18px;
opacity: 0.8;
> b {
margin-left: auto;
}
}
> .meter {
width: 100%;
overflow: hidden;
background: var(--X11);
border-radius: 8px;
> .val {
height: 4px;
transition: width .3s cubic-bezier(0.23, 1, 0.32, 1);
}
}
&:nth-child(1) {
> .meter > .val {
background: #f7796c;
}
}
&:nth-child(2) {
> .meter > .val {
background: #a1de41;
}
}
&:nth-child(3) {
> .meter > .val {
background: #41ddde;
}
}
} }
} }
} }
.monthAndYear,
.weekDay {
margin: 0;
line-height: 18px;
font-size: 0.9em;
}
.year,
.month {
margin: 0 4px;
}
.day {
margin: 10px 0;
line-height: 32px;
font-size: 1.75em;
}
.info {
display: block;
float: left;
width: 40%;
padding: 0 16px 0 0;
box-sizing: border-box;
}
.infoSection {
margin-bottom: 8px;
&:last-child {
margin-bottom: 4px;
}
&:nth-child(1) {
> .meter > .meterVal {
background: #f7796c;
}
}
&:nth-child(2) {
> .meter > .meterVal {
background: #a1de41;
}
}
&:nth-child(3) {
> .meter > .meterVal {
background: #41ddde;
}
}
}
.infoText {
display: flex;
margin: 0 0 2px 0;
font-size: 0.75em;
line-height: 18px;
opacity: 0.8;
}
.percentage {
margin-left: auto;
}
.meter {
width: 100%;
overflow: hidden;
background: var(--X11);
border-radius: 8px;
}
.meterVal {
height: 4px;
transition: width .3s cubic-bezier(0.23, 1, 0.32, 1);
}
</style> </style>

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="mkw-digitalClock _monospace" :class="{ _panel: !widgetProps.transparent }" :style="{ fontSize: `${widgetProps.fontSize}em` }"> <div class="_monospace" :class="[$style.root, { _panel: !widgetProps.transparent }]" :style="{ fontSize: `${widgetProps.fontSize}em` }">
<div v-if="widgetProps.showLabel" class="label">{{ tzAbbrev }}</div> <div v-if="widgetProps.showLabel" :class="$style.label">{{ tzAbbrev }}</div>
<div class="time"> <div>
<MkDigitalClock :show-ms="widgetProps.showMs" :offset="tzOffset"/> <MkDigitalClock :show-ms="widgetProps.showMs" :offset="tzOffset"/>
</div> </div>
<div v-if="widgetProps.showLabel" class="label">{{ tzOffsetLabel }}</div> <div v-if="widgetProps.showLabel" :class="$style.label">{{ tzOffsetLabel }}</div>
</div> </div>
</template> </template>
@@ -79,14 +79,14 @@ defineExpose<WidgetComponentExpose>({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.mkw-digitalClock { .root {
padding: 16px 0; padding: 16px 0;
text-align: center; text-align: center;
}
> .label { .label {
font-size: 65%; font-size: 65%;
opacity: 0.7; opacity: 0.7;
}
} }
</style> </style>

View File

@@ -3,9 +3,9 @@
<template #icon><i class="ti ti-note"></i></template> <template #icon><i class="ti ti-note"></i></template>
<template #header>{{ i18n.ts._widgets.memo }}</template> <template #header>{{ i18n.ts._widgets.memo }}</template>
<div class="otgbylcu"> <div :class="$style.root">
<textarea v-model="text" :placeholder="i18n.ts.placeholder" @input="onChange"></textarea> <textarea v-model="text" :class="$style.textarea" :placeholder="i18n.ts.placeholder" @input="onChange"></textarea>
<button :disabled="!changed" class="_buttonPrimary" @click="saveMemo">{{ i18n.ts.save }}</button> <button :class="$style.save" :disabled="!changed" class="_buttonPrimary" @click="saveMemo">{{ i18n.ts.save }}</button>
</div> </div>
</MkContainer> </MkContainer>
</template> </template>
@@ -68,45 +68,45 @@ defineExpose<WidgetComponentExpose>({
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" module>
.otgbylcu { .root {
padding-bottom: 28px + 16px; padding-bottom: 28px + 16px;
}
> textarea { .textarea {
display: block; display: block;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
min-width: 100%; min-width: 100%;
padding: 16px; padding: 16px;
color: var(--fg); color: var(--fg);
background: transparent; background: transparent;
border: none; border: none;
border-bottom: solid 0.5px var(--divider); border-bottom: solid 0.5px var(--divider);
border-radius: 0; border-radius: 0;
box-sizing: border-box; box-sizing: border-box;
font: inherit; font: inherit;
font-size: 0.9em; font-size: 0.9em;
&:focus-visible { &:focus-visible {
outline: none;
}
}
> button {
display: block;
position: absolute;
bottom: 8px;
right: 8px;
margin: 0;
padding: 0 10px;
height: 28px;
outline: none; outline: none;
border-radius: 4px; }
}
&:disabled { .save {
opacity: 0.7; display: block;
cursor: default; position: absolute;
} bottom: 8px;
right: 8px;
margin: 0;
padding: 0 10px;
height: 28px;
outline: none;
border-radius: 4px;
&:disabled {
opacity: 0.7;
cursor: default;
} }
} }
</style> </style>

View File

@@ -2555,7 +2555,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/node@npm:18.11.18": "@types/node@npm:18.11.18, @types/node@npm:^18.11.18":
version: 18.11.18 version: 18.11.18
resolution: "@types/node@npm:18.11.18" resolution: "@types/node@npm:18.11.18"
checksum: 03f17f9480f8d775c8a72da5ea7e9383db5f6d85aa5fefde90dd953a1449bd5e4ffde376f139da4f3744b4c83942166d2a7603969a6f8ea826edfb16e6e3b49d checksum: 03f17f9480f8d775c8a72da5ea7e9383db5f6d85aa5fefde90dd953a1449bd5e4ffde376f139da4f3744b4c83942166d2a7603969a6f8ea826edfb16e6e3b49d
@@ -8132,6 +8132,7 @@ __metadata:
"@types/gulp": 4.0.10 "@types/gulp": 4.0.10
"@types/gulp-rename": 2.0.1 "@types/gulp-rename": 2.0.1
"@types/matter-js": 0.18.2 "@types/matter-js": 0.18.2
"@types/node": ^18.11.18
"@types/punycode": 2.1.0 "@types/punycode": 2.1.0
"@types/sanitize-html": ^2.8.0 "@types/sanitize-html": ^2.8.0
"@types/seedrandom": 3.0.4 "@types/seedrandom": 3.0.4