Move file transfer restriction into network settings
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
cancelTransfer,
|
||||
uploadProgress,
|
||||
clipboardTextLimitBytes,
|
||||
fileTransfersAllowed,
|
||||
} from './client';
|
||||
const props = defineProps({ initialView: { type: String, default: 'inbox' } });
|
||||
const emit = defineEmits(['devices', 'send-item']);
|
||||
@@ -62,6 +63,9 @@ const compose = ref(false),
|
||||
preferencesOpen = ref(false),
|
||||
editing = ref(null),
|
||||
noteDraft = ref('');
|
||||
watch(fileTransfersAllowed, (allowed) => {
|
||||
if (!allowed && saveKind.value === 'file' && !saveTransfer.value) saveKind.value = 'clipboard';
|
||||
});
|
||||
const storage = ref({ usedBytes: 0, limitBytes: 0 });
|
||||
const securityAction = ref('none'),
|
||||
currentPassword = ref(''),
|
||||
@@ -292,6 +296,8 @@ async function save(values) {
|
||||
busy.value = true;
|
||||
let id = saveTransfer.value?.id;
|
||||
try {
|
||||
if (values.files && !fileTransfersAllowed.value && !id)
|
||||
throw Error('지금은 링크와 텍스트만 저장할 수 있어요.');
|
||||
const result = id
|
||||
? { requestId: id }
|
||||
: await api(
|
||||
@@ -598,6 +604,9 @@ async function resendVerification() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!fileTransfersAllowed" class="file-policy-note" role="status">
|
||||
새 파일 전송·저장은 제한되어 있어요. 링크와 텍스트는 계속 사용할 수 있어요.
|
||||
</p>
|
||||
<div class="account-tabs" role="tablist" aria-label="보관함 목록">
|
||||
<button
|
||||
role="tab"
|
||||
@@ -662,7 +671,11 @@ async function resendVerification() {
|
||||
<div class="item-footer">
|
||||
<span>[보관 만료] {{ expiryDate(item.expiresAt) }} 까지</span>
|
||||
<div>
|
||||
<button class="button small" @click="emit('send-item', item)">
|
||||
<button
|
||||
v-if="!item.file || fileTransfersAllowed"
|
||||
class="button small"
|
||||
@click="emit('send-item', item)"
|
||||
>
|
||||
<Send :size="14" />기기로 보내기</button
|
||||
><button v-if="item.file" class="button small" @click="act(() => download(item))">
|
||||
<Download :size="14" />다운로드</button
|
||||
@@ -733,10 +746,15 @@ async function resendVerification() {
|
||||
<X :size="20" />
|
||||
</button>
|
||||
</header>
|
||||
<p v-if="!fileTransfersAllowed && !saveKind" class="content-feedback" role="status">
|
||||
파일 전송이 일시 제한되어 링크와 텍스트만 저장할 수 있어요.
|
||||
</p>
|
||||
<div v-if="!saveKind" class="save-kind-choices">
|
||||
<button class="button" @click="saveKind = 'clipboard'">
|
||||
<Copy :size="22" />클립보드 저장</button
|
||||
><button class="button" @click="saveKind = 'file'"><FileIcon :size="22" />파일 저장</button>
|
||||
><button v-if="fileTransfersAllowed" class="button" @click="saveKind = 'file'">
|
||||
<FileIcon :size="22" />파일 저장
|
||||
</button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<label class="vault-note-label"
|
||||
@@ -747,6 +765,7 @@ async function resendVerification() {
|
||||
placeholder="예: 나중에 읽을 글"
|
||||
:disabled="Boolean(saveTransfer) || busy" /></label
|
||||
><SendComposer
|
||||
:files-allowed="fileTransfersAllowed"
|
||||
target-name="임시 보관함"
|
||||
target-key="vault-save"
|
||||
:save-mode="true"
|
||||
|
||||
@@ -5,8 +5,6 @@ import {
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
ArrowLeft,
|
||||
Pause,
|
||||
Play,
|
||||
Folder,
|
||||
Globe,
|
||||
HardDrive,
|
||||
@@ -64,19 +62,6 @@ async function reload() {
|
||||
notice.value = '최신 관리 정보를 불러왔어요.';
|
||||
});
|
||||
}
|
||||
async function pauseUploads() {
|
||||
await act(async () => {
|
||||
const settings = await api('/admin/settings', 'PATCH', {
|
||||
revision: overview.value.settings.revision,
|
||||
uploadsPaused: !overview.value.settings.uploadsPaused,
|
||||
});
|
||||
overview.value.settings = settings;
|
||||
notice.value = settings.uploadsPaused
|
||||
? '신규 업로드를 중지했어요. 기존 전송은 계속됩니다.'
|
||||
: '신규 업로드를 다시 허용했어요.';
|
||||
await refresh();
|
||||
});
|
||||
}
|
||||
function addBlockedIp(ip) {
|
||||
pendingBlockedIp.value = ip;
|
||||
view.value = 'network';
|
||||
@@ -186,19 +171,6 @@ onMounted(async () => {
|
||||
>
|
||||
</article>
|
||||
</div>
|
||||
<div class="admin-upload-control">
|
||||
<div>
|
||||
<strong>{{
|
||||
overview.settings.uploadsPaused ? '신규 업로드 중지됨' : '신규 업로드 허용 중'
|
||||
}}</strong>
|
||||
<p>이미 시작한 전송과 기존 파일 다운로드는 유지됩니다.</p>
|
||||
</div>
|
||||
<button class="button" :disabled="busy" @click="pauseUploads">
|
||||
<Play v-if="overview.settings.uploadsPaused" :size="16" /><Pause v-else :size="16" />{{
|
||||
overview.settings.uploadsPaused ? '업로드 재개' : '신규 업로드 중지'
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
<section v-if="overview.requests.length" class="admin-activity">
|
||||
<strong>진행 중인 업로드 요청</strong>
|
||||
<div v-for="request in overview.requests" :key="request.ip + ':' + request.startedAt">
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
spaceInviteCodeLength,
|
||||
locationSettings,
|
||||
clipboardTextLimitBytes,
|
||||
fileTransfersAllowed,
|
||||
publishLocation,
|
||||
clearLocation,
|
||||
reserveLocationAcceptance,
|
||||
@@ -510,7 +511,14 @@ async function moreVault() {
|
||||
vaultCursor.value = result.nextCursor;
|
||||
});
|
||||
}
|
||||
watch(fileTransfersAllowed, (allowed) => {
|
||||
if (!allowed && itemToSend.value?.file) itemToSend.value = null;
|
||||
});
|
||||
function prepareVault(item) {
|
||||
if (item.file && !fileTransfersAllowed.value) {
|
||||
showError(Error('지금은 링크와 텍스트만 보낼 수 있어요.'));
|
||||
return;
|
||||
}
|
||||
itemToSend.value = item;
|
||||
selected.value = null;
|
||||
sendOpen.value = false;
|
||||
@@ -519,6 +527,10 @@ function prepareVault(item) {
|
||||
notify('받을 기기를 선택해 주세요.');
|
||||
}
|
||||
async function forwardVault(item) {
|
||||
if (item.file && !fileTransfersAllowed.value) {
|
||||
showError(Error('지금은 링크와 텍스트만 보낼 수 있어요.'));
|
||||
return;
|
||||
}
|
||||
if (!selected.value) {
|
||||
itemToSend.value = item;
|
||||
tab.value = 'devices';
|
||||
@@ -680,6 +692,8 @@ async function closeReceivedText(item) {
|
||||
async function submitFiles(files) {
|
||||
await act(async () => {
|
||||
if (!selected.value) throw Error('받을 기기를 선택해 주세요.');
|
||||
if (!fileTransfersAllowed.value && currentTransfer.value?.status !== 'accepted')
|
||||
throw Error('지금은 링크와 텍스트만 보낼 수 있어요.');
|
||||
const target = selected.value;
|
||||
forwardedItem.value = null;
|
||||
itemToSend.value = null;
|
||||
@@ -987,10 +1001,17 @@ async function spaceAction(space, action, memberId) {
|
||||
><span class="request-file-size">{{ size(file.size) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p
|
||||
v-if="r.kind === 'file' && !fileTransfersAllowed"
|
||||
class="content-feedback"
|
||||
role="status"
|
||||
>
|
||||
파일 전송이 일시 제한되어 수락할 수 없어요.
|
||||
</p>
|
||||
<div class="incoming-actions">
|
||||
<button
|
||||
class="button primary small"
|
||||
:disabled="busy"
|
||||
:disabled="busy || (r.kind === 'file' && !fileTransfersAllowed)"
|
||||
@click="receiveAction(r, 'accept')"
|
||||
>
|
||||
<Check :size="16" /> 수락</button
|
||||
@@ -1026,6 +1047,9 @@ async function spaceAction(space, action, memberId) {
|
||||
<template v-if="tab === 'devices'">
|
||||
<section class="devices-stage">
|
||||
<section class="nearby-section">
|
||||
<p v-if="!fileTransfersAllowed" class="file-policy-note" role="status">
|
||||
파일 전송이 일시 제한되어 링크와 텍스트만 보낼 수 있어요.
|
||||
</p>
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>
|
||||
@@ -1220,6 +1244,7 @@ async function spaceAction(space, action, memberId) {
|
||||
</button>
|
||||
</header>
|
||||
<SendComposer
|
||||
:files-allowed="fileTransfersAllowed"
|
||||
:target-name="selected?.name"
|
||||
:target-key="selected?.id"
|
||||
:clipboard-key="selectionEpoch"
|
||||
|
||||
@@ -97,12 +97,14 @@ async function save() {
|
||||
<label
|
||||
>동시에 처리할 업로드 수<input
|
||||
v-model.number="draft.maxConcurrentUploads"
|
||||
aria-label="동시에 처리할 업로드 수"
|
||||
type="number"
|
||||
min="1"
|
||||
max="16"
|
||||
required
|
||||
:disabled="busy"
|
||||
/></label>
|
||||
/><small>서버 전체에서 동시에 처리할 업로드 요청 수</small></label
|
||||
>
|
||||
<label v-for="limit in limits" :key="limit.key"
|
||||
>{{ limit.label }} (GB)<input
|
||||
v-model.number="draft[limit.key]"
|
||||
@@ -127,6 +129,7 @@ async function save() {
|
||||
<dt>동시에 처리할 업로드 수</dt>
|
||||
<dd>
|
||||
<strong>{{ settings.maxConcurrentUploads }}개</strong>
|
||||
<small>서버 전체에서 동시에 처리할 업로드 요청 수</small>
|
||||
</dd>
|
||||
</div>
|
||||
<div v-for="limit in limits" :key="limit.key">
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue';
|
||||
import { Plus, Trash2, X, RefreshCw, Pencil } from '@lucide/vue';
|
||||
import { api, showError } from './client';
|
||||
import { formatAdminDate as date, formatCountry as country } from './displayFormat';
|
||||
import {
|
||||
formatAdminDate as date,
|
||||
formatCountry as country,
|
||||
formatSize as size,
|
||||
} from './displayFormat';
|
||||
const props = defineProps({ settings: { type: Object, required: true }, suggestedIp: String });
|
||||
const emit = defineEmits(['saved', 'activity', 'consumed']);
|
||||
const draft = reactive({}),
|
||||
@@ -42,6 +46,7 @@ const reasonName = (reason) =>
|
||||
})[reason] || reason;
|
||||
function fill(settings) {
|
||||
for (const key of [
|
||||
'uploadsPaused',
|
||||
'blacklistEnabled',
|
||||
'allowedCountries',
|
||||
'allowedIps',
|
||||
@@ -230,6 +235,30 @@ function modalKey(event) {
|
||||
</p>
|
||||
<p v-if="notice" role="status" class="admin-notice">{{ notice }}</p>
|
||||
<form @submit.prevent="save">
|
||||
<section class="network-section" aria-labelledby="file-policy-title">
|
||||
<header>
|
||||
<h2 id="file-policy-title">파일 전송 금지</h2>
|
||||
<button
|
||||
v-if="editing"
|
||||
type="button"
|
||||
role="switch"
|
||||
class="network-switch"
|
||||
:aria-checked="draft.uploadsPaused"
|
||||
aria-label="파일 전송 금지"
|
||||
:disabled="busy"
|
||||
@click="draft.uploadsPaused = !draft.uploadsPaused"
|
||||
>
|
||||
<span></span>{{ draft.uploadsPaused ? '켜짐' : '꺼짐' }}
|
||||
</button>
|
||||
<span v-else class="admin-muted">{{
|
||||
draft.uploadsPaused ? '켜짐 · 링크와 텍스트만 허용' : '꺼짐 · 파일 전송 허용'
|
||||
}}</span>
|
||||
</header>
|
||||
<p class="admin-muted">
|
||||
켜면 새 파일 전송·보관함 파일 저장을 막고 사용자 화면의 파일 선택 메뉴를 숨깁니다. 링크와
|
||||
텍스트는 계속 보낼 수 있습니다. 이미 수락한 파일 전송과 기존 파일 다운로드는 유지됩니다.
|
||||
</p>
|
||||
</section>
|
||||
<section class="network-section" aria-labelledby="automatic-title">
|
||||
<header>
|
||||
<h2 id="automatic-title">자동 블랙리스트</h2>
|
||||
@@ -329,7 +358,7 @@ function modalKey(event) {
|
||||
</div>
|
||||
<div>
|
||||
<dt>요청한 파일 용량</dt>
|
||||
<dd>{{ uploadGb }}GB</dd>
|
||||
<dd>{{ size(draft.autoUploadBytes) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>자동 차단 시간</dt>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { classifyClipboard, clipboardCharacterCount } from '@hop/shared';
|
||||
import { mayReadClipboardAutomatically } from './clipboardAccess';
|
||||
const props = defineProps({
|
||||
targetName: String,
|
||||
filesAllowed: { type: Boolean, default: true },
|
||||
textLimitBytes: { type: Number, default: 262144 },
|
||||
targetKey: String,
|
||||
clipboardKey: Number,
|
||||
@@ -47,7 +48,16 @@ const source = ref('url'),
|
||||
editor = ref(null),
|
||||
compositionKind = ref(null);
|
||||
let generation = 0;
|
||||
const clipboardOnly = computed(() => ['url', 'clipboard'].includes(props.contentMode));
|
||||
const canSelectFiles = computed(
|
||||
() =>
|
||||
props.filesAllowed || (props.transfer?.kind === 'file' && props.transfer.status === 'accepted'),
|
||||
);
|
||||
const clipboardOnly = computed(
|
||||
() => !canSelectFiles.value || ['url', 'clipboard'].includes(props.contentMode),
|
||||
);
|
||||
const availableVaultItems = computed(() =>
|
||||
(props.vaultItems || []).filter((item) => !item.file || props.filesAllowed),
|
||||
);
|
||||
const classified = computed(() => classifyClipboard(urlInput.value));
|
||||
const editorKind = computed(() => compositionKind.value ?? classified.value.kind);
|
||||
const contentBytes = computed(() => new TextEncoder().encode(urlInput.value).length);
|
||||
@@ -72,8 +82,8 @@ const ready = computed(() =>
|
||||
source.value === 'url'
|
||||
? validClipboard.value && !reading.value && !compositionKind.value
|
||||
: source.value === 'file'
|
||||
? files.value.length > 0
|
||||
: Boolean(selectedVault.value),
|
||||
? canSelectFiles.value && files.value.length > 0
|
||||
: Boolean(selectedVault.value && (!selectedVault.value.file || props.filesAllowed)),
|
||||
);
|
||||
async function loadClipboard(force = false, explicit = false) {
|
||||
if (
|
||||
@@ -127,9 +137,9 @@ watch(
|
||||
selectedVault.value = null;
|
||||
clipboardManaged.value = false;
|
||||
feedback.value = '';
|
||||
source.value = props.contentMode === 'file' ? 'file' : 'url';
|
||||
source.value = props.contentMode === 'file' && canSelectFiles.value ? 'file' : 'url';
|
||||
}
|
||||
if (props.initialItem) {
|
||||
if (props.initialItem && (!props.initialItem.file || props.filesAllowed)) {
|
||||
source.value = 'vault';
|
||||
selectedVault.value = props.initialItem;
|
||||
} else if (current[0] !== previous?.[0] || current[1] !== previous?.[1])
|
||||
@@ -164,7 +174,7 @@ watch(
|
||||
() => props.vaultEnabled,
|
||||
(enabled) => {
|
||||
if (!enabled && source.value === 'vault') {
|
||||
source.value = 'file';
|
||||
source.value = props.filesAllowed ? 'file' : 'url';
|
||||
selectedVault.value = null;
|
||||
}
|
||||
},
|
||||
@@ -186,7 +196,20 @@ watch(
|
||||
(owner, previous) => {
|
||||
if (owner !== previous && source.value === 'vault') {
|
||||
selectedVault.value = null;
|
||||
source.value = 'file';
|
||||
source.value = props.filesAllowed ? 'file' : 'url';
|
||||
}
|
||||
},
|
||||
);
|
||||
watch(
|
||||
() => props.filesAllowed,
|
||||
(allowed) => {
|
||||
if (allowed || props.transfer) return;
|
||||
if (source.value === 'file' || selectedVault.value?.file) {
|
||||
generation++;
|
||||
reading.value = false;
|
||||
files.value = [];
|
||||
selectedVault.value = null;
|
||||
source.value = 'url';
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -208,7 +231,7 @@ function choose(list) {
|
||||
feedback.value = '';
|
||||
}
|
||||
function pickFiles() {
|
||||
if (mutable.value && fileInput.value) {
|
||||
if (mutable.value && !clipboardOnly.value && fileInput.value) {
|
||||
fileInput.value.value = '';
|
||||
fileInput.value.click();
|
||||
}
|
||||
@@ -235,14 +258,17 @@ function toVault() {
|
||||
emit('vault-open');
|
||||
}
|
||||
function selectVault(item) {
|
||||
if (mutable.value) selectedVault.value = item;
|
||||
if (mutable.value && (!item.file || props.filesAllowed)) selectedVault.value = item;
|
||||
}
|
||||
function pasted(event) {
|
||||
if (!mutable.value) return;
|
||||
if (event.clipboardData?.files.length) {
|
||||
event.preventDefault();
|
||||
choose(event.clipboardData.files);
|
||||
return;
|
||||
if (!clipboardOnly.value) {
|
||||
choose(event.clipboardData.files);
|
||||
return;
|
||||
}
|
||||
if (!event.clipboardData.getData('text/plain')) return;
|
||||
}
|
||||
if (props.contentMode === 'file' || props.transfer) return;
|
||||
const value = event.clipboardData?.getData('text/plain');
|
||||
@@ -454,7 +480,7 @@ function fileProgress(index) {
|
||||
</div>
|
||||
<template v-else
|
||||
><button
|
||||
v-for="item in vaultItems"
|
||||
v-for="item in availableVaultItems"
|
||||
:key="item.accessId"
|
||||
class="vault-choice"
|
||||
:class="{ chosen: selectedVault?.accessId === item.accessId }"
|
||||
@@ -473,8 +499,14 @@ function fileProgress(index) {
|
||||
></span
|
||||
><Check v-if="selectedVault?.accessId === item.accessId" :size="16" />
|
||||
</button>
|
||||
<p v-if="!vaultItems?.length" class="content-feedback">
|
||||
{{ busy ? '보관함을 불러오고 있어요.' : '보관함이 비어 있어요.' }}
|
||||
<p v-if="!availableVaultItems.length" class="content-feedback">
|
||||
{{
|
||||
busy
|
||||
? '보관함을 불러오고 있어요.'
|
||||
: !filesAllowed
|
||||
? '보낼 링크나 텍스트가 없어요.'
|
||||
: '보관함이 비어 있어요.'
|
||||
}}
|
||||
</p>
|
||||
<button
|
||||
v-if="vaultCursor"
|
||||
@@ -533,7 +565,12 @@ function fileProgress(index) {
|
||||
<button class="text-button room-leave" @click="emit('cancel')">전송 취소</button>
|
||||
</div>
|
||||
<div v-else-if="!contentMode && !transfer" class="content-switches">
|
||||
<button v-if="source !== 'file'" class="text-button" :disabled="!mutable" @click="pickFiles">
|
||||
<button
|
||||
v-if="!clipboardOnly && source !== 'file'"
|
||||
class="text-button"
|
||||
:disabled="!mutable"
|
||||
@click="pickFiles"
|
||||
>
|
||||
<FileIcon :size="14" />대신 파일 선택하기</button
|
||||
><button v-if="source !== 'url'" class="text-button" :disabled="!mutable" @click="toLink">
|
||||
<Link :size="14" />대신 클립보드에서 가져오기</button
|
||||
@@ -546,6 +583,13 @@ function fileProgress(index) {
|
||||
<Inbox :size="14" />보관함에서 가져오기
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!filesAllowed" class="content-feedback" role="status">
|
||||
{{
|
||||
transfer?.kind === 'file' && transfer.status === 'accepted'
|
||||
? '새 파일 전송은 제한되어 있어요. 이미 수락한 전송은 계속할 수 있어요.'
|
||||
: '파일 전송이 일시 제한되어 링크와 텍스트만 보낼 수 있어요.'
|
||||
}}
|
||||
</p>
|
||||
<p class="consent-note">
|
||||
{{ saveMode ? '저장한 자료는 최대 7일 후 삭제됩니다.' : '상대가 수락한 후에만 전달됩니다.' }}
|
||||
</p>
|
||||
|
||||
@@ -32,6 +32,19 @@ let pendingTimer;
|
||||
let deviceInfo = {};
|
||||
export const nextCursor = ref(null);
|
||||
export const clipboardTextLimitBytes = ref(262144);
|
||||
export const fileTransfersAllowed = ref(false);
|
||||
let policyRevision = -1;
|
||||
function applyServicePolicy(policy) {
|
||||
if (
|
||||
!policy ||
|
||||
!Number.isSafeInteger(policy.revision) ||
|
||||
policy.revision < policyRevision ||
|
||||
typeof policy.fileTransfersAllowed !== 'boolean'
|
||||
)
|
||||
return;
|
||||
policyRevision = policy.revision;
|
||||
fileTransfersAllowed.value = policy.fileTransfersAllowed;
|
||||
}
|
||||
export const spaceInviteCodeLength = ref(4);
|
||||
export const locationSettings = ref({ enabled: false, radiusMeters: 100, refreshMs: 120000 });
|
||||
export async function publishLocation(position) {
|
||||
@@ -129,6 +142,7 @@ export async function api(path, method = 'GET', body, key, signal) {
|
||||
export async function bootstrap() {
|
||||
const init = async () => {
|
||||
const s = await api('/guest/session', 'POST', {});
|
||||
applyServicePolicy(s.servicePolicy);
|
||||
device.value = s.device;
|
||||
csrfToken.value = s.csrfToken;
|
||||
sessionExpiresAt.value = s.expiresAt;
|
||||
@@ -166,6 +180,7 @@ function connect() {
|
||||
account.value = null;
|
||||
window.dispatchEvent(new Event('hop-account-revoked'));
|
||||
});
|
||||
socket.on('service:policy', applyServicePolicy);
|
||||
socket.on('service:blocked', () => showError(Error('이 접속은 서비스 이용이 제한되었습니다.')));
|
||||
socket.on('connect_error', (err) => {
|
||||
connected.value = false;
|
||||
@@ -176,6 +191,7 @@ function connect() {
|
||||
if (err.message !== 'SESSION_EXPIRED' || reauthorizing) return;
|
||||
reauthorizing = (async () => {
|
||||
const session = await api('/guest/session', 'POST', {});
|
||||
applyServicePolicy(session.servicePolicy);
|
||||
device.value = session.device;
|
||||
csrfToken.value = session.csrfToken;
|
||||
sessionExpiresAt.value = session.expiresAt;
|
||||
@@ -229,6 +245,7 @@ export function showError(err) {
|
||||
}
|
||||
export const refresh = createRefreshQueue(async () => {
|
||||
const snapshot = await api('/device/snapshot');
|
||||
applyServicePolicy(snapshot.servicePolicy);
|
||||
device.value = snapshot.device;
|
||||
peers.value = snapshot.devices;
|
||||
clipboardTextLimitBytes.value = snapshot.clipboardTextLimitBytes ?? 262144;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
const sizeNumber = new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
});
|
||||
export function formatSize(bytes) {
|
||||
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)}GB`;
|
||||
if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(1)}MB`;
|
||||
if (bytes >= 1000) return `${(bytes / 1000).toFixed(1).replace(/\.0$/, '')}KB`;
|
||||
if (bytes >= 1e9) return `${sizeNumber.format(bytes / 1e9)}GB`;
|
||||
if (bytes >= 1e6) return `${sizeNumber.format(bytes / 1e6)}MB`;
|
||||
if (bytes >= 1000) return `${sizeNumber.format(bytes / 1000).replace(/\.0$/, '')}KB`;
|
||||
return bytes === 0 ? '0KB' : '1KB 미만';
|
||||
}
|
||||
const countryNames = new Intl.DisplayNames(['ko'], { type: 'region' });
|
||||
@@ -45,7 +49,7 @@ export function auditChangeLines(change) {
|
||||
].filter(Boolean);
|
||||
}
|
||||
const names = {
|
||||
uploadsPaused: '신규 업로드',
|
||||
uploadsPaused: '파일 전송 금지',
|
||||
maxConcurrentUploads: '동시에 처리할 업로드 수',
|
||||
totalStorageLimitMb: '전체 파일 저장 한도',
|
||||
accountBytes: '로그인 보관함 계정당 한도',
|
||||
@@ -65,8 +69,8 @@ export function auditChangeLines(change) {
|
||||
const value = (item) =>
|
||||
change.field === 'uploadsPaused'
|
||||
? item
|
||||
? '중지'
|
||||
: '허용'
|
||||
? '켜짐'
|
||||
: '꺼짐'
|
||||
: typeof item === 'boolean'
|
||||
? item
|
||||
? '켜짐'
|
||||
|
||||
@@ -3916,7 +3916,6 @@ a.button {
|
||||
}
|
||||
.admin-heading,
|
||||
.admin-toolbar,
|
||||
.admin-upload-control,
|
||||
.admin-file-heading,
|
||||
.admin-file footer {
|
||||
display: flex;
|
||||
@@ -3975,20 +3974,6 @@ a.button {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.admin-upload-control {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.admin-upload-control p {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
margin: 7px 0 0;
|
||||
}
|
||||
.admin-upload-control button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.admin-notice {
|
||||
font-size: 14px;
|
||||
margin: 16px 0;
|
||||
@@ -4088,6 +4073,7 @@ a.button {
|
||||
.admin-settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-items: start;
|
||||
gap: 18px;
|
||||
}
|
||||
.admin-limit-details {
|
||||
@@ -4099,6 +4085,7 @@ a.button {
|
||||
}
|
||||
.admin-limit-details > div {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 3px;
|
||||
}
|
||||
.admin-limit-details dt {
|
||||
@@ -4181,11 +4168,6 @@ a.button {
|
||||
.admin-stats strong {
|
||||
font-size: 22px;
|
||||
}
|
||||
.admin-upload-control {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
.admin-settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -4484,3 +4466,10 @@ a.button {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.file-policy-note {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user