@brftech/filex-core 0.1.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/ArchiveViewer-CJdaw5x3.js +99 -0
- package/dist/ArchiveViewer-CJdaw5x3.js.map +1 -0
- package/dist/CsvViewer-QYx_tePQ.js +132 -0
- package/dist/CsvViewer-QYx_tePQ.js.map +1 -0
- package/dist/DrawioViewer-GWt0TyB7.js +141 -0
- package/dist/DrawioViewer-GWt0TyB7.js.map +1 -0
- package/dist/EpubViewer-Je0ywklQ.js +146 -0
- package/dist/EpubViewer-Je0ywklQ.js.map +1 -0
- package/dist/IpynbViewer-TsB4gqP-.js +175 -0
- package/dist/IpynbViewer-TsB4gqP-.js.map +1 -0
- package/dist/MermaidViewer-CXfeDS-7.js +128 -0
- package/dist/MermaidViewer-CXfeDS-7.js.map +1 -0
- package/dist/PsdViewer-VtPpxKVe.js +113 -0
- package/dist/PsdViewer-VtPpxKVe.js.map +1 -0
- package/dist/TiffViewer-OINn9bbb.js +133 -0
- package/dist/TiffViewer-OINn9bbb.js.map +1 -0
- package/dist/UTIF-Cjsrlm0l.js +3104 -0
- package/dist/UTIF-Cjsrlm0l.js.map +1 -0
- package/dist/Viewer3D-xvvyiWHd.js +61 -0
- package/dist/Viewer3D-xvvyiWHd.js.map +1 -0
- package/dist/_commonjsHelpers-DaMA6jEr.js +9 -0
- package/dist/_commonjsHelpers-DaMA6jEr.js.map +1 -0
- package/dist/filex-core.js +26 -0
- package/dist/filex-core.js.map +1 -0
- package/dist/filex-core.umd.cjs +285 -0
- package/dist/filex-core.umd.cjs.map +1 -0
- package/dist/index-CmSYzt6L.js +5404 -0
- package/dist/index-CmSYzt6L.js.map +1 -0
- package/dist/index-UFULWo35.js +10736 -0
- package/dist/index-UFULWo35.js.map +1 -0
- package/dist/index.d.ts +1089 -0
- package/dist/katex-yuB6V-q6.js +11616 -0
- package/dist/katex-yuB6V-q6.js.map +1 -0
- package/dist/papaparse.min-VB1HBwYX.js +441 -0
- package/dist/papaparse.min-VB1HBwYX.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/useViewerFetch-czqbd2Lj.js +25 -0
- package/dist/useViewerFetch-czqbd2Lj.js.map +1 -0
- package/package.json +113 -0
- package/src/FileExplorer.vue +1872 -0
- package/src/components/Breadcrumb.vue +282 -0
- package/src/components/ContextMenu.vue +152 -0
- package/src/components/GridView.vue +166 -0
- package/src/components/ListView.vue +212 -0
- package/src/components/PendingOpsTray.vue +91 -0
- package/src/components/RecentlyOpened.vue +155 -0
- package/src/components/StarButton.vue +101 -0
- package/src/components/TagPicker.vue +184 -0
- package/src/components/Toolbar.vue +220 -0
- package/src/components/UploadProgress.vue +78 -0
- package/src/composables/useFileApi.ts +692 -0
- package/src/composables/useKeyboardShortcuts.ts +144 -0
- package/src/composables/useLocale.ts +60 -0
- package/src/composables/useMonacoLoader.ts +98 -0
- package/src/composables/usePendingOps.ts +200 -0
- package/src/composables/useSelection.ts +81 -0
- package/src/composables/useUploadChunked.ts +250 -0
- package/src/composables/useViewerFetch.ts +58 -0
- package/src/index.ts +80 -0
- package/src/locales/en.ts +122 -0
- package/src/locales/index.ts +12 -0
- package/src/locales/tr.ts +122 -0
- package/src/modals/ConvertModal.vue +254 -0
- package/src/modals/DeleteConfirmModal.vue +32 -0
- package/src/modals/Modal.vue +104 -0
- package/src/modals/NewFolderModal.vue +64 -0
- package/src/modals/PermissionsModal.vue +815 -0
- package/src/modals/PreviewModal.vue +1070 -0
- package/src/modals/RenameModal.vue +58 -0
- package/src/modals/ShareModal.vue +135 -0
- package/src/styles/base.css +1224 -0
- package/src/styles/variables.css +90 -0
- package/src/types/ExplorerConfig.ts +282 -0
- package/src/types/FileNode.ts +136 -0
- package/src/types/index.ts +27 -0
- package/src/types/peers.d.ts +45 -0
- package/src/viewers/ArchiveViewer.vue +186 -0
- package/src/viewers/CsvViewer.vue +319 -0
- package/src/viewers/DrawioViewer.vue +243 -0
- package/src/viewers/EpubViewer.vue +307 -0
- package/src/viewers/IpynbViewer.vue +365 -0
- package/src/viewers/MermaidViewer.vue +259 -0
- package/src/viewers/PdfViewer.vue +562 -0
- package/src/viewers/PsdViewer.vue +264 -0
- package/src/viewers/TiffViewer.vue +273 -0
- package/src/viewers/Viewer3D.vue +120 -0
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Access modal — one popup combining "İzinler" (per-user RBAC grants,
|
|
3
|
+
// owner-only) and "Bağlantı ile paylaş" (public share link, editor+). Opened
|
|
4
|
+
// from the explorer's unified "Paylaş / İzinler" action. The layout is a fixed
|
|
5
|
+
// header/tabs with a single scrollable body so the popup never grows into one
|
|
6
|
+
// long scroll. Styling uses the SFC's --fe-* theme variables (light/dark).
|
|
7
|
+
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
|
|
8
|
+
import type { FileApi, Grant, UserSuggestion } from '../composables/useFileApi';
|
|
9
|
+
import type { ShareInfo } from '../types/FileNode';
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
api: FileApi;
|
|
13
|
+
path: string; // adapter://rel of the target item
|
|
14
|
+
isDir?: boolean; // folder → grants cascade; file → no `/…` inheritance hint
|
|
15
|
+
size?: number; // bytes, for the share-mail body (files only)
|
|
16
|
+
locale?: 'tr' | 'en';
|
|
17
|
+
}>();
|
|
18
|
+
const emit = defineEmits<{ (e: 'close'): void }>();
|
|
19
|
+
|
|
20
|
+
const tr = computed(() => (props.locale ?? 'tr') !== 'en');
|
|
21
|
+
function L(t: string, e: string): string {
|
|
22
|
+
return tr.value ? t : e;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Split adapter://rel for a friendlier path chip.
|
|
26
|
+
const pathParts = computed(() => {
|
|
27
|
+
const m = /^([^:]+):\/\/(.*)$/.exec(props.path);
|
|
28
|
+
const adapter = m ? m[1] : '';
|
|
29
|
+
const rel = m ? m[2] : props.path;
|
|
30
|
+
const segs = rel.split('/').filter(Boolean);
|
|
31
|
+
return { adapter, name: segs.length ? segs[segs.length - 1] : adapter, rel };
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
type Tab = 'perms' | 'share' | 'drop';
|
|
35
|
+
const tab = ref<Tab>('perms');
|
|
36
|
+
const canManage = ref(false); // owner/admin → can see the permissions tab
|
|
37
|
+
|
|
38
|
+
// ── permissions state ──
|
|
39
|
+
const loading = ref(true);
|
|
40
|
+
const err = ref('');
|
|
41
|
+
const direct = ref<Grant[]>([]);
|
|
42
|
+
const inherited = ref<Grant[]>([]);
|
|
43
|
+
const storageRbac = ref(true);
|
|
44
|
+
const email = ref('');
|
|
45
|
+
const level = ref<'viewer' | 'editor' | 'owner'>('viewer');
|
|
46
|
+
const busy = ref(false);
|
|
47
|
+
const notice = ref('');
|
|
48
|
+
const noAccount = ref(false);
|
|
49
|
+
const createRole = ref<'user' | 'viewer'>('user');
|
|
50
|
+
const inviteResult = ref<{ tempPassword?: string } | null>(null);
|
|
51
|
+
const suggestions = ref<UserSuggestion[]>([]);
|
|
52
|
+
const showSuggest = ref(false);
|
|
53
|
+
let searchTimer: ReturnType<typeof setTimeout> | undefined;
|
|
54
|
+
|
|
55
|
+
const levels: Array<{ v: 'viewer' | 'editor' | 'owner'; l: string; d: string }> = [
|
|
56
|
+
{ v: 'viewer', l: L('Görüntüleyen', 'Viewer'), d: L('görüntüle + indir', 'view + download') },
|
|
57
|
+
{ v: 'editor', l: L('Düzenleyen', 'Editor'), d: L('oku + yaz + sil', 'read + write + delete') },
|
|
58
|
+
{ v: 'owner', l: L('Sahip', 'Owner'), d: L('düzenle + izin yönet', 'edit + manage access') },
|
|
59
|
+
];
|
|
60
|
+
function levelLabel(v: string): string {
|
|
61
|
+
return levels.find((o) => o.v === v)?.l ?? v;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── share state ──
|
|
65
|
+
const shares = ref<ShareInfo[]>([]);
|
|
66
|
+
const shareBusy = ref(false);
|
|
67
|
+
const sharePwd = ref(false);
|
|
68
|
+
const shareExpiry = ref(0); // days; 0 = never
|
|
69
|
+
const shareResult = ref<{ url: string; pin?: string | null } | null>(null);
|
|
70
|
+
const shareErr = ref('');
|
|
71
|
+
const copied = ref('');
|
|
72
|
+
// prefilled recipient when the owner chose "share link" for a no-account email
|
|
73
|
+
const shareMailTo = ref('');
|
|
74
|
+
const shareMailBusy = ref(false);
|
|
75
|
+
const shareMailNotice = ref('');
|
|
76
|
+
|
|
77
|
+
const expiryOptions = [
|
|
78
|
+
{ v: 0, l: L('Süresiz', 'Never') },
|
|
79
|
+
{ v: 1, l: L('1 gün', '1 day') },
|
|
80
|
+
{ v: 7, l: L('7 gün', '7 days') },
|
|
81
|
+
{ v: 30, l: L('30 gün', '30 days') },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// ── file-drop (public upload link) state ──
|
|
85
|
+
const dropPwd = ref(false);
|
|
86
|
+
const dropExpiry = ref(0); // days; 0 = never
|
|
87
|
+
const dropShowAdv = ref(false);
|
|
88
|
+
const dropMaxFiles = ref<string>('');
|
|
89
|
+
const dropMaxSizeMB = ref<string>('');
|
|
90
|
+
const dropAllowedExt = ref<string>('');
|
|
91
|
+
const dropAskName = ref(true);
|
|
92
|
+
const dropBusy = ref(false);
|
|
93
|
+
const dropErr = ref('');
|
|
94
|
+
const dropResult = ref<{ url: string; pin?: string | null } | null>(null);
|
|
95
|
+
const dropMailTo = ref('');
|
|
96
|
+
const dropMailBusy = ref(false);
|
|
97
|
+
const dropMailNotice = ref('');
|
|
98
|
+
|
|
99
|
+
// splitEmails turns a free-text recipient field into a deduped address list —
|
|
100
|
+
// comma / semicolon / whitespace separated, so one input handles many people.
|
|
101
|
+
function splitEmails(raw: string): string[] {
|
|
102
|
+
const seen = new Set<string>();
|
|
103
|
+
const out: string[] = [];
|
|
104
|
+
for (const part of raw.split(/[,;\s]+/)) {
|
|
105
|
+
const e = part.trim().toLowerCase();
|
|
106
|
+
if (e && e.includes('@') && !seen.has(e)) {
|
|
107
|
+
seen.add(e);
|
|
108
|
+
out.push(e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// mailResultNotice renders a sent/failed summary from the share-mail response.
|
|
115
|
+
function mailResultNotice(res: { sent?: string[]; failed?: string[] }): string {
|
|
116
|
+
const sent = res.sent?.length ?? 0;
|
|
117
|
+
const failed = res.failed?.length ?? 0;
|
|
118
|
+
if (failed === 0) return L(`E-posta gönderildi ✓ (${sent})`, `Email sent ✓ (${sent})`);
|
|
119
|
+
return L(`${sent} gönderildi, ${failed} başarısız`, `${sent} sent, ${failed} failed`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function reload() {
|
|
123
|
+
loading.value = true;
|
|
124
|
+
err.value = '';
|
|
125
|
+
try {
|
|
126
|
+
const r = await props.api.listPermissions(props.path);
|
|
127
|
+
direct.value = r.direct ?? [];
|
|
128
|
+
inherited.value = r.inherited ?? [];
|
|
129
|
+
storageRbac.value = r.storage_rbac;
|
|
130
|
+
canManage.value = true;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
// 403 = caller is editor (not owner): no permissions tab, share only.
|
|
133
|
+
const st = (e as { status?: number }).status;
|
|
134
|
+
if (st === 403) {
|
|
135
|
+
canManage.value = false;
|
|
136
|
+
// Editor (not owner): no permissions tab → fall back to the link tab.
|
|
137
|
+
tab.value = 'share';
|
|
138
|
+
} else {
|
|
139
|
+
err.value = e instanceof Error ? e.message : String(e);
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
loading.value = false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function reloadShares() {
|
|
146
|
+
try {
|
|
147
|
+
const r = await props.api.listShares(props.path);
|
|
148
|
+
shares.value = r.shares ?? [];
|
|
149
|
+
} catch {
|
|
150
|
+
shares.value = [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
onMounted(async () => {
|
|
154
|
+
await reload();
|
|
155
|
+
await reloadShares();
|
|
156
|
+
});
|
|
157
|
+
onBeforeUnmount(() => {
|
|
158
|
+
if (searchTimer) clearTimeout(searchTimer);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
function onEmailInput() {
|
|
162
|
+
noAccount.value = false;
|
|
163
|
+
inviteResult.value = null;
|
|
164
|
+
notice.value = '';
|
|
165
|
+
const q = email.value.trim();
|
|
166
|
+
if (searchTimer) clearTimeout(searchTimer);
|
|
167
|
+
if (q.length < 1) {
|
|
168
|
+
suggestions.value = [];
|
|
169
|
+
showSuggest.value = false;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
searchTimer = setTimeout(async () => {
|
|
173
|
+
try {
|
|
174
|
+
const r = await props.api.searchUsers(q);
|
|
175
|
+
suggestions.value = r.users ?? [];
|
|
176
|
+
showSuggest.value = suggestions.value.length > 0;
|
|
177
|
+
} catch {
|
|
178
|
+
showSuggest.value = false;
|
|
179
|
+
}
|
|
180
|
+
}, 180);
|
|
181
|
+
}
|
|
182
|
+
async function pickUser(u: UserSuggestion) {
|
|
183
|
+
showSuggest.value = false;
|
|
184
|
+
email.value = u.email;
|
|
185
|
+
let lvl = level.value;
|
|
186
|
+
if (u.role === 'viewer' && lvl !== 'viewer') lvl = 'viewer';
|
|
187
|
+
busy.value = true;
|
|
188
|
+
notice.value = '';
|
|
189
|
+
try {
|
|
190
|
+
await props.api.addPermission({ path: props.path, user_id: u.id, level: lvl, is_dir: !!props.isDir });
|
|
191
|
+
email.value = '';
|
|
192
|
+
suggestions.value = [];
|
|
193
|
+
await reload();
|
|
194
|
+
notice.value = L('Yetki verildi.', 'Access granted.');
|
|
195
|
+
} catch (e) {
|
|
196
|
+
notice.value = e instanceof Error ? e.message : String(e);
|
|
197
|
+
} finally {
|
|
198
|
+
busy.value = false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function submitEmail() {
|
|
202
|
+
const addr = email.value.trim().toLowerCase();
|
|
203
|
+
if (!addr || !addr.includes('@')) {
|
|
204
|
+
notice.value = L('Geçerli bir e-posta girin.', 'Enter a valid email.');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
showSuggest.value = false;
|
|
208
|
+
busy.value = true;
|
|
209
|
+
notice.value = '';
|
|
210
|
+
inviteResult.value = null;
|
|
211
|
+
try {
|
|
212
|
+
const res = await props.api.resolveEmail(addr);
|
|
213
|
+
if (res.found && res.user) {
|
|
214
|
+
let lvl = level.value;
|
|
215
|
+
if (res.user.role === 'viewer' && lvl !== 'viewer') lvl = 'viewer';
|
|
216
|
+
await props.api.addPermission({ path: props.path, user_id: res.user.id, level: lvl, is_dir: !!props.isDir });
|
|
217
|
+
email.value = '';
|
|
218
|
+
await reload();
|
|
219
|
+
notice.value = L('Yetki verildi.', 'Access granted.');
|
|
220
|
+
} else {
|
|
221
|
+
noAccount.value = true;
|
|
222
|
+
}
|
|
223
|
+
} catch (e) {
|
|
224
|
+
notice.value = e instanceof Error ? e.message : String(e);
|
|
225
|
+
} finally {
|
|
226
|
+
busy.value = false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function inviteCreateUser() {
|
|
230
|
+
busy.value = true;
|
|
231
|
+
notice.value = '';
|
|
232
|
+
try {
|
|
233
|
+
const r = await props.api.invitePermission({
|
|
234
|
+
path: props.path, email: email.value.trim().toLowerCase(),
|
|
235
|
+
level: level.value, create_user: true, role: createRole.value, is_dir: !!props.isDir,
|
|
236
|
+
locale: props.locale ?? 'tr',
|
|
237
|
+
});
|
|
238
|
+
inviteResult.value = { tempPassword: r.temp_password };
|
|
239
|
+
notice.value = r.emailed
|
|
240
|
+
? L('Kullanıcı açıldı, davet e-postası gönderildi.', 'User created, invite emailed.')
|
|
241
|
+
: L('Kullanıcı açıldı. Geçici parolayı iletin.', 'User created. Share the temp password.');
|
|
242
|
+
noAccount.value = false; email.value = '';
|
|
243
|
+
await reload();
|
|
244
|
+
} catch (e) {
|
|
245
|
+
notice.value = e instanceof Error ? e.message : String(e);
|
|
246
|
+
} finally {
|
|
247
|
+
busy.value = false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// "Sadece paylaş" → jump to the share tab with the address prefilled so the
|
|
251
|
+
// owner creates a link (with their chosen expiry/PIN) and mails it there.
|
|
252
|
+
function gotoShareWithMail() {
|
|
253
|
+
shareMailTo.value = email.value.trim().toLowerCase();
|
|
254
|
+
noAccount.value = false;
|
|
255
|
+
notice.value = '';
|
|
256
|
+
tab.value = 'share';
|
|
257
|
+
}
|
|
258
|
+
async function changeLevel(g: Grant, newLevel: string) {
|
|
259
|
+
if (newLevel === g.level) return;
|
|
260
|
+
busy.value = true;
|
|
261
|
+
try { await props.api.updatePermission(g.id, newLevel); await reload(); }
|
|
262
|
+
catch (e) { notice.value = e instanceof Error ? e.message : String(e); }
|
|
263
|
+
finally { busy.value = false; }
|
|
264
|
+
}
|
|
265
|
+
async function removeGrant(g: Grant) {
|
|
266
|
+
busy.value = true;
|
|
267
|
+
try { await props.api.deletePermission(g.id); await reload(); }
|
|
268
|
+
catch (e) { notice.value = e instanceof Error ? e.message : String(e); }
|
|
269
|
+
finally { busy.value = false; }
|
|
270
|
+
}
|
|
271
|
+
function glabel(g: Grant): string {
|
|
272
|
+
return g.user_display_name || g.user_email || `#${g.user_id}`;
|
|
273
|
+
}
|
|
274
|
+
function ginitial(g: Grant): string {
|
|
275
|
+
return (g.user_display_name || g.user_email || '?').charAt(0).toUpperCase();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── share actions ──
|
|
279
|
+
function expiresAtISO(): string | null {
|
|
280
|
+
if (!shareExpiry.value) return null;
|
|
281
|
+
return new Date(Date.now() + shareExpiry.value * 86400000).toISOString();
|
|
282
|
+
}
|
|
283
|
+
async function createLink() {
|
|
284
|
+
shareBusy.value = true;
|
|
285
|
+
shareErr.value = '';
|
|
286
|
+
shareResult.value = null;
|
|
287
|
+
shareMailNotice.value = '';
|
|
288
|
+
try {
|
|
289
|
+
const r = await props.api.createShare({
|
|
290
|
+
path: props.path,
|
|
291
|
+
password: sharePwd.value,
|
|
292
|
+
expires_at: expiresAtISO(),
|
|
293
|
+
});
|
|
294
|
+
shareResult.value = { url: r.share.url, pin: r.share.password_pin ?? null };
|
|
295
|
+
await reloadShares();
|
|
296
|
+
} catch (e) {
|
|
297
|
+
shareErr.value = e instanceof Error ? e.message : String(e);
|
|
298
|
+
} finally {
|
|
299
|
+
shareBusy.value = false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async function sendShareMail() {
|
|
303
|
+
const list = splitEmails(shareMailTo.value);
|
|
304
|
+
if (!list.length) {
|
|
305
|
+
shareMailNotice.value = L('Geçerli bir e-posta girin.', 'Enter a valid email.');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (!shareResult.value?.url) return;
|
|
309
|
+
shareMailBusy.value = true;
|
|
310
|
+
shareMailNotice.value = '';
|
|
311
|
+
try {
|
|
312
|
+
const res = await props.api.shareMail({
|
|
313
|
+
path: props.path, emails: list, url: shareResult.value.url,
|
|
314
|
+
pin: shareResult.value.pin ?? undefined,
|
|
315
|
+
expires_days: shareExpiry.value || undefined,
|
|
316
|
+
locale: props.locale ?? 'tr',
|
|
317
|
+
is_dir: !!props.isDir,
|
|
318
|
+
size: props.size,
|
|
319
|
+
});
|
|
320
|
+
shareMailNotice.value = mailResultNotice(res);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
const detail = (e as { detail?: string }).detail ?? '';
|
|
323
|
+
if (detail.includes('not_configured')) {
|
|
324
|
+
shareMailNotice.value = L('SMTP ayarlı/doğrulanmış değil — linki elle iletin.', 'SMTP not set up/verified — share the link manually.');
|
|
325
|
+
} else if (detail.includes('send_failed')) {
|
|
326
|
+
shareMailNotice.value = L('Gönderilemedi (geçici hata) — tekrar deneyin.', 'Send failed (temporary) — please retry.');
|
|
327
|
+
} else {
|
|
328
|
+
shareMailNotice.value = e instanceof Error ? e.message : String(e);
|
|
329
|
+
}
|
|
330
|
+
} finally {
|
|
331
|
+
shareMailBusy.value = false;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── file-drop (upload link) actions ──
|
|
336
|
+
function dropExpiresAtISO(): string | null {
|
|
337
|
+
if (!dropExpiry.value) return null;
|
|
338
|
+
return new Date(Date.now() + dropExpiry.value * 86400000).toISOString();
|
|
339
|
+
}
|
|
340
|
+
async function createDropLink() {
|
|
341
|
+
dropBusy.value = true;
|
|
342
|
+
dropErr.value = '';
|
|
343
|
+
dropResult.value = null;
|
|
344
|
+
dropMailNotice.value = '';
|
|
345
|
+
try {
|
|
346
|
+
const drop_settings: Record<string, unknown> = { ask_name: dropAskName.value };
|
|
347
|
+
if (dropMaxFiles.value) drop_settings.max_files = Number(dropMaxFiles.value);
|
|
348
|
+
if (dropMaxSizeMB.value) drop_settings.max_file_size_mb = Number(dropMaxSizeMB.value);
|
|
349
|
+
const exts = dropAllowedExt.value.split(/[,\s]+/).map((s) => s.trim().replace(/^\./, '')).filter(Boolean);
|
|
350
|
+
if (exts.length) drop_settings.allowed_ext = exts;
|
|
351
|
+
const r = await props.api.createShare({
|
|
352
|
+
path: props.path,
|
|
353
|
+
kind: 'drop',
|
|
354
|
+
password: dropPwd.value,
|
|
355
|
+
expires_at: dropExpiresAtISO(),
|
|
356
|
+
drop_settings,
|
|
357
|
+
});
|
|
358
|
+
dropResult.value = { url: r.share.url, pin: r.share.password_pin ?? null };
|
|
359
|
+
} catch (e) {
|
|
360
|
+
dropErr.value = e instanceof Error ? e.message : String(e);
|
|
361
|
+
} finally {
|
|
362
|
+
dropBusy.value = false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function sendDropMail() {
|
|
366
|
+
const list = splitEmails(dropMailTo.value);
|
|
367
|
+
if (!list.length) {
|
|
368
|
+
dropMailNotice.value = L('Geçerli bir e-posta girin.', 'Enter a valid email.');
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (!dropResult.value?.url) return;
|
|
372
|
+
dropMailBusy.value = true;
|
|
373
|
+
dropMailNotice.value = '';
|
|
374
|
+
try {
|
|
375
|
+
const res = await props.api.shareMail({
|
|
376
|
+
path: props.path, emails: list, url: dropResult.value.url,
|
|
377
|
+
pin: dropResult.value.pin ?? undefined,
|
|
378
|
+
expires_days: dropExpiry.value || undefined,
|
|
379
|
+
locale: props.locale ?? 'tr',
|
|
380
|
+
is_dir: true,
|
|
381
|
+
mode: 'drop',
|
|
382
|
+
});
|
|
383
|
+
dropMailNotice.value = mailResultNotice(res);
|
|
384
|
+
} catch (e) {
|
|
385
|
+
const detail = (e as { detail?: string }).detail ?? '';
|
|
386
|
+
if (detail.includes('not_configured')) {
|
|
387
|
+
dropMailNotice.value = L('SMTP ayarlı/doğrulanmış değil — linki elle iletin.', 'SMTP not set up/verified — share the link manually.');
|
|
388
|
+
} else {
|
|
389
|
+
dropMailNotice.value = e instanceof Error ? e.message : String(e);
|
|
390
|
+
}
|
|
391
|
+
} finally {
|
|
392
|
+
dropMailBusy.value = false;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async function revoke(s: ShareInfo) {
|
|
396
|
+
shareBusy.value = true;
|
|
397
|
+
try { await props.api.revokeShare(s.uuid); await reloadShares(); }
|
|
398
|
+
catch (e) { shareErr.value = e instanceof Error ? e.message : String(e); }
|
|
399
|
+
finally { shareBusy.value = false; }
|
|
400
|
+
}
|
|
401
|
+
function copy(text: string, tag = 'url') {
|
|
402
|
+
navigator.clipboard?.writeText(text);
|
|
403
|
+
copied.value = tag;
|
|
404
|
+
setTimeout(() => { if (copied.value === tag) copied.value = ''; }, 1400);
|
|
405
|
+
}
|
|
406
|
+
</script>
|
|
407
|
+
|
|
408
|
+
<template>
|
|
409
|
+
<div class="fx-perm-overlay" @click.self="emit('close')">
|
|
410
|
+
<div class="fx-perm-modal">
|
|
411
|
+
<header class="fx-perm-head">
|
|
412
|
+
<div class="fx-perm-title">
|
|
413
|
+
<span class="fx-perm-ico" aria-hidden="true">{{ isDir ? '📁' : '📄' }}</span>
|
|
414
|
+
<div class="fx-perm-titletext">
|
|
415
|
+
<h3>{{ L('Paylaş / İzinler', 'Share / Permissions') }}</h3>
|
|
416
|
+
<span class="fx-perm-sub" :title="path">{{ pathParts.name }}<span class="fx-perm-subdim"> · {{ pathParts.adapter }}</span></span>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
<button class="fx-perm-x" @click="emit('close')" aria-label="close">✕</button>
|
|
420
|
+
</header>
|
|
421
|
+
|
|
422
|
+
<div class="fx-perm-tabs">
|
|
423
|
+
<button
|
|
424
|
+
v-if="canManage"
|
|
425
|
+
class="fx-perm-tab"
|
|
426
|
+
:class="{ 'is-active': tab === 'perms' }"
|
|
427
|
+
@click="tab = 'perms'"
|
|
428
|
+
>{{ L('Kişiler', 'People') }}</button>
|
|
429
|
+
<button
|
|
430
|
+
class="fx-perm-tab"
|
|
431
|
+
:class="{ 'is-active': tab === 'share' }"
|
|
432
|
+
@click="tab = 'share'"
|
|
433
|
+
>{{ L('Bağlantı', 'Link') }}</button>
|
|
434
|
+
<button
|
|
435
|
+
v-if="isDir"
|
|
436
|
+
class="fx-perm-tab"
|
|
437
|
+
:class="{ 'is-active': tab === 'drop' }"
|
|
438
|
+
@click="tab = 'drop'"
|
|
439
|
+
>{{ L('Dosya İste', 'Request files') }}</button>
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
<div class="fx-perm-body">
|
|
443
|
+
<!-- ───────── Permissions tab ───────── -->
|
|
444
|
+
<template v-if="tab === 'perms' && canManage">
|
|
445
|
+
<div v-if="!storageRbac" class="fx-perm-warn">
|
|
446
|
+
{{ L('Bu diskte RBAC kapalı — izinler yalnızca RBAC açık disklerde geçerli.', 'RBAC is off on this storage — grants only apply when RBAC is enabled.') }}
|
|
447
|
+
</div>
|
|
448
|
+
<div v-if="err" class="fx-perm-warn">{{ err }}</div>
|
|
449
|
+
<div v-if="loading" class="fx-perm-muted">{{ L('Yükleniyor…', 'Loading…') }}</div>
|
|
450
|
+
<template v-else>
|
|
451
|
+
<!-- Add person (primary action, on top) -->
|
|
452
|
+
<div class="fx-perm-addcard">
|
|
453
|
+
<div class="fx-perm-add">
|
|
454
|
+
<div class="fx-perm-emailwrap">
|
|
455
|
+
<input v-model="email" type="email" class="fx-perm-input" autocomplete="off"
|
|
456
|
+
:placeholder="L('İsim veya e-posta', 'Name or email')"
|
|
457
|
+
@input="onEmailInput" @keyup.enter="submitEmail" @focus="onEmailInput" />
|
|
458
|
+
<ul v-if="showSuggest" class="fx-perm-suggest">
|
|
459
|
+
<li v-for="u in suggestions" :key="u.id" @mousedown.prevent="pickUser(u)">
|
|
460
|
+
<span class="fx-perm-suggest-av">{{ (u.display_name || u.email).charAt(0).toUpperCase() }}</span>
|
|
461
|
+
<span class="fx-perm-suggest-txt">
|
|
462
|
+
<span class="fx-perm-suggest-name">{{ u.display_name || u.email }}</span>
|
|
463
|
+
<span class="fx-perm-suggest-meta">{{ u.email }} · {{ u.role }}</span>
|
|
464
|
+
</span>
|
|
465
|
+
</li>
|
|
466
|
+
</ul>
|
|
467
|
+
</div>
|
|
468
|
+
<select v-model="level" class="fx-perm-sel" :title="levels.find(o => o.v === level)?.d">
|
|
469
|
+
<option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
470
|
+
</select>
|
|
471
|
+
<button class="fx-perm-btn fx-perm-btn--primary" :disabled="busy" @click="submitEmail">
|
|
472
|
+
{{ L('Ekle', 'Add') }}
|
|
473
|
+
</button>
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
<div v-if="noAccount" class="fx-perm-invite">
|
|
477
|
+
<p class="fx-perm-muted">{{ L('Bu e-postada hesap yok. Ne yapmak istersiniz?', 'No account for this email — what next?') }}</p>
|
|
478
|
+
<div class="fx-perm-invite-actions">
|
|
479
|
+
<div class="fx-perm-invite-row">
|
|
480
|
+
<select v-model="createRole" class="fx-perm-sel">
|
|
481
|
+
<option value="user">{{ L('Kullanıcı', 'User') }}</option>
|
|
482
|
+
<option value="viewer">{{ L('Görüntüleyen', 'Viewer') }}</option>
|
|
483
|
+
</select>
|
|
484
|
+
<button class="fx-perm-btn fx-perm-btn--primary" :disabled="busy" @click="inviteCreateUser">
|
|
485
|
+
{{ L('Kullanıcı oluştur + yetki ver', 'Create user + grant') }}
|
|
486
|
+
</button>
|
|
487
|
+
</div>
|
|
488
|
+
<button class="fx-perm-btn fx-perm-btn--ghost" :disabled="busy" @click="gotoShareWithMail">
|
|
489
|
+
{{ L('Sadece paylaşım linki gönder →', 'Just send a share link →') }}
|
|
490
|
+
</button>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
<div v-if="notice" class="fx-perm-notice">{{ notice }}</div>
|
|
494
|
+
<div v-if="inviteResult?.tempPassword" class="fx-perm-reveal">
|
|
495
|
+
{{ L('Geçici parola:', 'Temp password:') }} <code>{{ inviteResult.tempPassword }}</code>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
|
|
499
|
+
<!-- People with access -->
|
|
500
|
+
<div class="fx-perm-section">
|
|
501
|
+
<h4>{{ L('Erişimi olanlar', 'People with access') }}</h4>
|
|
502
|
+
<div v-if="!direct.length && !inherited.length" class="fx-perm-empty">
|
|
503
|
+
{{ L('Henüz kimseyle paylaşılmadı.', 'Not shared with anyone yet.') }}
|
|
504
|
+
</div>
|
|
505
|
+
<div v-for="g in direct" :key="'d' + g.id" class="fx-perm-person">
|
|
506
|
+
<span class="fx-perm-av">{{ ginitial(g) }}</span>
|
|
507
|
+
<span class="fx-perm-user" :title="g.user_email">{{ glabel(g) }}</span>
|
|
508
|
+
<select class="fx-perm-sel fx-perm-sel--sm" :value="g.level"
|
|
509
|
+
@change="changeLevel(g, ($event.target as HTMLSelectElement).value)">
|
|
510
|
+
<option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
511
|
+
</select>
|
|
512
|
+
<button class="fx-perm-del" :disabled="busy" :title="L('Kaldır', 'Remove')" @click="removeGrant(g)">✕</button>
|
|
513
|
+
</div>
|
|
514
|
+
<div v-for="g in inherited" :key="'i' + g.id" class="fx-perm-person fx-perm-inh">
|
|
515
|
+
<span class="fx-perm-av fx-perm-av--dim">{{ ginitial(g) }}</span>
|
|
516
|
+
<span class="fx-perm-user" :title="g.user_email">{{ glabel(g) }}</span>
|
|
517
|
+
<span class="fx-perm-badge">{{ levelLabel(g.level) }}</span>
|
|
518
|
+
<span class="fx-perm-from" :title="L('Üst klasörden gelir', 'Inherited from') + ': ' + (g.path_prefix || '/')">
|
|
519
|
+
↳ {{ g.path_prefix || '/' }}
|
|
520
|
+
</span>
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
</template>
|
|
524
|
+
</template>
|
|
525
|
+
|
|
526
|
+
<!-- ───────── Share tab ───────── -->
|
|
527
|
+
<template v-if="tab === 'share'">
|
|
528
|
+
<div class="fx-perm-addcard">
|
|
529
|
+
<div class="fx-perm-share-opts">
|
|
530
|
+
<label class="fx-perm-check">
|
|
531
|
+
<input type="checkbox" v-model="sharePwd" />
|
|
532
|
+
{{ L('PIN ile koru', 'Protect with a PIN') }}
|
|
533
|
+
</label>
|
|
534
|
+
<label class="fx-perm-expiry">
|
|
535
|
+
<span class="fx-perm-muted">{{ L('Süre', 'Expiry') }}</span>
|
|
536
|
+
<select v-model.number="shareExpiry" class="fx-perm-sel fx-perm-sel--sm">
|
|
537
|
+
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
538
|
+
</select>
|
|
539
|
+
</label>
|
|
540
|
+
<button class="fx-perm-btn fx-perm-btn--primary" :disabled="shareBusy" @click="createLink">
|
|
541
|
+
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
542
|
+
</button>
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
<div v-if="shareErr" class="fx-perm-warn">{{ shareErr }}</div>
|
|
546
|
+
|
|
547
|
+
<div v-if="shareResult" class="fx-perm-result">
|
|
548
|
+
<div class="fx-perm-linkrow">
|
|
549
|
+
<a :href="shareResult.url" target="_blank" rel="noopener" class="fx-perm-link">{{ shareResult.url }}</a>
|
|
550
|
+
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(shareResult.url, 'new')">
|
|
551
|
+
{{ copied === 'new' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
552
|
+
</button>
|
|
553
|
+
</div>
|
|
554
|
+
<div v-if="shareResult.pin" class="fx-perm-pin">
|
|
555
|
+
<span>PIN: <code>{{ shareResult.pin }}</code></span>
|
|
556
|
+
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(shareResult.pin, 'sharepin')">
|
|
557
|
+
{{ copied === 'sharepin' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
558
|
+
</button>
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
<!-- send by email (one or more, comma/space separated) -->
|
|
562
|
+
<div class="fx-perm-mailrow">
|
|
563
|
+
<input v-model="shareMailTo" type="text" class="fx-perm-input" autocomplete="off"
|
|
564
|
+
:placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendShareMail" />
|
|
565
|
+
<button class="fx-perm-btn" :disabled="shareMailBusy" @click="sendShareMail">
|
|
566
|
+
{{ L('Gönder', 'Send') }}
|
|
567
|
+
</button>
|
|
568
|
+
</div>
|
|
569
|
+
<div v-if="shareMailNotice" class="fx-perm-notice">{{ shareMailNotice }}</div>
|
|
570
|
+
</div>
|
|
571
|
+
<p v-else-if="shareMailTo" class="fx-perm-hint">
|
|
572
|
+
{{ L('Aşağıdan bir bağlantı oluşturun, ardından', 'Create a link below, then it will be sent to') }}
|
|
573
|
+
<strong>{{ shareMailTo }}</strong> {{ L('adresine gönderin.', '.') }}
|
|
574
|
+
</p>
|
|
575
|
+
</div>
|
|
576
|
+
|
|
577
|
+
<div class="fx-perm-section">
|
|
578
|
+
<h4>{{ L('Mevcut bağlantılar', 'Existing links') }}</h4>
|
|
579
|
+
<div v-if="!shares.length" class="fx-perm-empty">{{ L('Yok', 'None') }}</div>
|
|
580
|
+
<div v-for="s in shares" :key="s.uuid" class="fx-perm-person">
|
|
581
|
+
<span class="fx-perm-user fx-perm-link" :title="s.url">{{ s.url }}</span>
|
|
582
|
+
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(s.url, s.uuid)">
|
|
583
|
+
{{ copied === s.uuid ? L('✓', '✓') : L('Kopyala', 'Copy') }}
|
|
584
|
+
</button>
|
|
585
|
+
<button class="fx-perm-del" :disabled="shareBusy" :title="L('İptal', 'Revoke')" @click="revoke(s)">✕</button>
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
</template>
|
|
589
|
+
|
|
590
|
+
<!-- ───────── File-drop (upload link) tab ───────── -->
|
|
591
|
+
<template v-if="tab === 'drop'">
|
|
592
|
+
<div class="fx-perm-addcard">
|
|
593
|
+
<p class="fx-perm-muted fx-perm-dropintro">
|
|
594
|
+
{{ L('Bu klasöre herkesin dosya YÜKLEYEBİLECEĞİ herkese açık bir bağlantı. Yükleyenler klasördeki mevcut dosyaları göremez.', 'A public link that lets anyone UPLOAD files into this folder. Uploaders never see the folder\'s existing files.') }}
|
|
595
|
+
</p>
|
|
596
|
+
<div class="fx-perm-share-opts">
|
|
597
|
+
<label class="fx-perm-check">
|
|
598
|
+
<input type="checkbox" v-model="dropPwd" />
|
|
599
|
+
{{ L('PIN ile koru', 'Protect with a PIN') }}
|
|
600
|
+
</label>
|
|
601
|
+
<label class="fx-perm-expiry">
|
|
602
|
+
<span class="fx-perm-muted">{{ L('Süre', 'Expiry') }}</span>
|
|
603
|
+
<select v-model.number="dropExpiry" class="fx-perm-sel fx-perm-sel--sm">
|
|
604
|
+
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
605
|
+
</select>
|
|
606
|
+
</label>
|
|
607
|
+
<button class="fx-perm-btn fx-perm-btn--primary" :disabled="dropBusy" @click="createDropLink">
|
|
608
|
+
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
609
|
+
</button>
|
|
610
|
+
</div>
|
|
611
|
+
|
|
612
|
+
<button type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-advtoggle" @click="dropShowAdv = !dropShowAdv">
|
|
613
|
+
{{ dropShowAdv ? L('Gelişmiş ▲', 'Advanced ▲') : L('Gelişmiş ▼', 'Advanced ▼') }}
|
|
614
|
+
</button>
|
|
615
|
+
<div v-if="dropShowAdv" class="fx-perm-adv">
|
|
616
|
+
<label class="fx-perm-adv-row">
|
|
617
|
+
<span class="fx-perm-muted">{{ L('En fazla dosya', 'Max files') }}</span>
|
|
618
|
+
<input v-model="dropMaxFiles" type="number" min="1" class="fx-perm-input fx-perm-input--sm" placeholder="20" />
|
|
619
|
+
</label>
|
|
620
|
+
<label class="fx-perm-adv-row">
|
|
621
|
+
<span class="fx-perm-muted">{{ L('Dosya başı MB', 'MB / file') }}</span>
|
|
622
|
+
<input v-model="dropMaxSizeMB" type="number" min="1" class="fx-perm-input fx-perm-input--sm" placeholder="500" />
|
|
623
|
+
</label>
|
|
624
|
+
<label class="fx-perm-adv-row">
|
|
625
|
+
<span class="fx-perm-muted">{{ L('İzinli türler', 'Allowed types') }}</span>
|
|
626
|
+
<input v-model="dropAllowedExt" type="text" class="fx-perm-input fx-perm-input--sm" :placeholder="L('hepsi (örn. pdf, jpg)', 'all (e.g. pdf, jpg)')" />
|
|
627
|
+
</label>
|
|
628
|
+
<label class="fx-perm-check">
|
|
629
|
+
<input type="checkbox" v-model="dropAskName" />
|
|
630
|
+
{{ L('Yükleyenin adını sor', 'Ask uploader name') }}
|
|
631
|
+
</label>
|
|
632
|
+
</div>
|
|
633
|
+
|
|
634
|
+
<div v-if="dropErr" class="fx-perm-warn">{{ dropErr }}</div>
|
|
635
|
+
|
|
636
|
+
<div v-if="dropResult" class="fx-perm-result">
|
|
637
|
+
<div class="fx-perm-linkrow">
|
|
638
|
+
<a :href="dropResult.url" target="_blank" rel="noopener" class="fx-perm-link">{{ dropResult.url }}</a>
|
|
639
|
+
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(dropResult.url, 'drop')">
|
|
640
|
+
{{ copied === 'drop' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
641
|
+
</button>
|
|
642
|
+
</div>
|
|
643
|
+
<div v-if="dropResult.pin" class="fx-perm-pin">
|
|
644
|
+
<span>PIN: <code>{{ dropResult.pin }}</code></span>
|
|
645
|
+
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(dropResult.pin, 'droppin')">
|
|
646
|
+
{{ copied === 'droppin' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
647
|
+
</button>
|
|
648
|
+
</div>
|
|
649
|
+
|
|
650
|
+
<!-- email the upload link to one or more people -->
|
|
651
|
+
<div class="fx-perm-mailrow">
|
|
652
|
+
<input v-model="dropMailTo" type="text" class="fx-perm-input" autocomplete="off"
|
|
653
|
+
:placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendDropMail" />
|
|
654
|
+
<button class="fx-perm-btn" :disabled="dropMailBusy" @click="sendDropMail">
|
|
655
|
+
{{ L('Gönder', 'Send') }}
|
|
656
|
+
</button>
|
|
657
|
+
</div>
|
|
658
|
+
<div v-if="dropMailNotice" class="fx-perm-notice">{{ dropMailNotice }}</div>
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
</template>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
</template>
|
|
666
|
+
|
|
667
|
+
<style scoped>
|
|
668
|
+
.fx-perm-overlay {
|
|
669
|
+
position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5);
|
|
670
|
+
display: flex; align-items: center; justify-content: center; z-index: 10000;
|
|
671
|
+
font-family: var(--fe-font);
|
|
672
|
+
}
|
|
673
|
+
.fx-perm-modal {
|
|
674
|
+
display: flex; flex-direction: column;
|
|
675
|
+
background: var(--fe-bg); color: var(--fe-text);
|
|
676
|
+
width: min(520px, 94vw); max-height: 86vh;
|
|
677
|
+
border: 1px solid var(--fe-border); border-radius: var(--fe-radius-lg, 14px);
|
|
678
|
+
box-shadow: var(--fe-shadow); font-size: 14px; overflow: hidden;
|
|
679
|
+
}
|
|
680
|
+
/* header + tabs are fixed; only the body scrolls */
|
|
681
|
+
.fx-perm-head {
|
|
682
|
+
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
|
683
|
+
padding: 16px 18px 12px; flex: none;
|
|
684
|
+
}
|
|
685
|
+
.fx-perm-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
686
|
+
.fx-perm-ico { font-size: 22px; line-height: 1; }
|
|
687
|
+
.fx-perm-titletext { display: flex; flex-direction: column; min-width: 0; }
|
|
688
|
+
.fx-perm-title h3 { margin: 0; font-size: 15px; font-weight: 600; color: var(--fe-text); }
|
|
689
|
+
.fx-perm-sub {
|
|
690
|
+
font-size: 12px; color: var(--fe-text); max-width: 340px;
|
|
691
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
692
|
+
}
|
|
693
|
+
.fx-perm-subdim { color: var(--fe-text-muted); }
|
|
694
|
+
.fx-perm-x {
|
|
695
|
+
background: none; border: none; font-size: 17px; cursor: pointer;
|
|
696
|
+
color: var(--fe-text-muted); flex: none; line-height: 1; padding: 2px 4px; border-radius: 6px;
|
|
697
|
+
}
|
|
698
|
+
.fx-perm-x:hover { color: var(--fe-text); background: var(--fe-bg-hover); }
|
|
699
|
+
.fx-perm-tabs { display: flex; gap: 2px; padding: 0 18px; border-bottom: 1px solid var(--fe-border); flex: none; }
|
|
700
|
+
.fx-perm-tab {
|
|
701
|
+
background: none; border: none; border-bottom: 2px solid transparent;
|
|
702
|
+
padding: 8px 12px; cursor: pointer; color: var(--fe-text-muted); font-size: 13px;
|
|
703
|
+
font-family: inherit; font-weight: 500;
|
|
704
|
+
}
|
|
705
|
+
.fx-perm-tab:hover { color: var(--fe-text); }
|
|
706
|
+
.fx-perm-tab.is-active { color: var(--fe-primary); border-bottom-color: var(--fe-primary); }
|
|
707
|
+
.fx-perm-body { flex: 1 1 auto; overflow-y: auto; padding: 14px 18px 18px; }
|
|
708
|
+
|
|
709
|
+
.fx-perm-addcard {
|
|
710
|
+
padding: 12px; border-radius: var(--fe-radius-md, 10px);
|
|
711
|
+
background: var(--fe-bg-elev); border: 1px solid var(--fe-border);
|
|
712
|
+
}
|
|
713
|
+
.fx-perm-section { margin-top: 16px; }
|
|
714
|
+
.fx-perm-section h4 {
|
|
715
|
+
margin: 0 0 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fe-text-muted);
|
|
716
|
+
}
|
|
717
|
+
.fx-perm-empty { color: var(--fe-text-muted); font-size: 13px; padding: 8px 2px; }
|
|
718
|
+
|
|
719
|
+
/* person rows */
|
|
720
|
+
.fx-perm-person { display: flex; align-items: center; gap: 10px; padding: 7px 2px; }
|
|
721
|
+
.fx-perm-person + .fx-perm-person { border-top: 1px solid var(--fe-border); }
|
|
722
|
+
.fx-perm-inh { color: var(--fe-text-muted); }
|
|
723
|
+
.fx-perm-av {
|
|
724
|
+
width: 28px; height: 28px; flex: none; border-radius: 50%;
|
|
725
|
+
display: flex; align-items: center; justify-content: center;
|
|
726
|
+
background: var(--fe-primary); color: var(--fe-text-on-primary, #fff);
|
|
727
|
+
font-size: 12px; font-weight: 600;
|
|
728
|
+
}
|
|
729
|
+
.fx-perm-av--dim { background: var(--fe-bg-hover); color: var(--fe-text-muted); }
|
|
730
|
+
.fx-perm-user { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fe-text); min-width: 0; }
|
|
731
|
+
.fx-perm-from { font-size: 11px; color: var(--fe-text-muted); white-space: nowrap; }
|
|
732
|
+
.fx-perm-badge {
|
|
733
|
+
font-size: 11px; padding: 2px 9px; border-radius: 999px;
|
|
734
|
+
background: var(--fe-bg-hover); color: var(--fe-text);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/* inputs / selects / buttons */
|
|
738
|
+
.fx-perm-sel, .fx-perm-input {
|
|
739
|
+
padding: 7px 9px; border-radius: var(--fe-radius-sm, 7px); border: 1px solid var(--fe-border);
|
|
740
|
+
background: var(--fe-bg); color: var(--fe-text); font-size: 13px; font-family: inherit;
|
|
741
|
+
}
|
|
742
|
+
.fx-perm-sel--sm { padding: 5px 7px; font-size: 12px; }
|
|
743
|
+
.fx-perm-sel:focus, .fx-perm-input:focus { outline: none; border-color: var(--fe-primary); }
|
|
744
|
+
.fx-perm-add { display: flex; gap: 8px; align-items: stretch; }
|
|
745
|
+
.fx-perm-emailwrap { position: relative; flex: 1; min-width: 120px; }
|
|
746
|
+
.fx-perm-input { width: 100%; box-sizing: border-box; }
|
|
747
|
+
.fx-perm-suggest {
|
|
748
|
+
position: absolute; top: calc(100% + 3px); left: 0; right: 0; z-index: 5; margin: 0; padding: 4px;
|
|
749
|
+
list-style: none; background: var(--fe-bg); border: 1px solid var(--fe-border);
|
|
750
|
+
border-radius: var(--fe-radius-sm, 7px); box-shadow: var(--fe-shadow); max-height: 210px; overflow: auto;
|
|
751
|
+
}
|
|
752
|
+
.fx-perm-suggest li { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
|
|
753
|
+
.fx-perm-suggest li:hover { background: var(--fe-bg-hover); }
|
|
754
|
+
.fx-perm-suggest-av {
|
|
755
|
+
width: 24px; height: 24px; flex: none; border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
|
756
|
+
background: var(--fe-bg-hover); color: var(--fe-text); font-size: 11px; font-weight: 600;
|
|
757
|
+
}
|
|
758
|
+
.fx-perm-suggest-txt { display: flex; flex-direction: column; min-width: 0; }
|
|
759
|
+
.fx-perm-suggest-name { color: var(--fe-text); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
760
|
+
.fx-perm-suggest-meta { color: var(--fe-text-muted); font-size: 11px; }
|
|
761
|
+
.fx-perm-check { display: inline-flex; gap: 6px; align-items: center; font-size: 13px; color: var(--fe-text); cursor: pointer; }
|
|
762
|
+
.fx-perm-btn {
|
|
763
|
+
padding: 7px 13px; border-radius: var(--fe-radius-sm, 7px); border: 1px solid var(--fe-border);
|
|
764
|
+
background: var(--fe-bg); color: var(--fe-text); font-size: 13px; font-family: inherit; cursor: pointer; white-space: nowrap;
|
|
765
|
+
}
|
|
766
|
+
.fx-perm-btn:hover:not(:disabled) { background: var(--fe-bg-hover); }
|
|
767
|
+
.fx-perm-btn:disabled { opacity: 0.55; cursor: default; }
|
|
768
|
+
.fx-perm-btn--sm { padding: 5px 10px; font-size: 12px; }
|
|
769
|
+
.fx-perm-btn--primary { background: var(--fe-primary); border-color: var(--fe-primary); color: var(--fe-text-on-primary, #fff); }
|
|
770
|
+
.fx-perm-btn--primary:hover:not(:disabled) { background: var(--fe-primary-hover, var(--fe-primary)); filter: brightness(1.05); }
|
|
771
|
+
.fx-perm-btn--ghost { background: none; border-color: transparent; color: var(--fe-primary); padding-left: 2px; }
|
|
772
|
+
.fx-perm-btn--ghost:hover:not(:disabled) { background: none; text-decoration: underline; }
|
|
773
|
+
.fx-perm-del { background: none; border: none; color: var(--fe-danger); cursor: pointer; font-size: 14px; flex: none; padding: 2px 4px; }
|
|
774
|
+
.fx-perm-del:hover:not(:disabled) { filter: brightness(1.15); }
|
|
775
|
+
|
|
776
|
+
/* invite sub-flow */
|
|
777
|
+
.fx-perm-invite {
|
|
778
|
+
margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--fe-border);
|
|
779
|
+
}
|
|
780
|
+
.fx-perm-invite-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
|
|
781
|
+
.fx-perm-invite-row { display: flex; gap: 8px; align-items: stretch; }
|
|
782
|
+
.fx-perm-invite-row .fx-perm-btn--primary { flex: 1; }
|
|
783
|
+
|
|
784
|
+
/* share result */
|
|
785
|
+
.fx-perm-share-opts { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
|
786
|
+
.fx-perm-expiry { display: inline-flex; gap: 6px; align-items: center; font-size: 13px; }
|
|
787
|
+
.fx-perm-share-opts .fx-perm-btn--primary { margin-left: auto; }
|
|
788
|
+
.fx-perm-result { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--fe-border); }
|
|
789
|
+
.fx-perm-linkrow { display: flex; gap: 8px; align-items: center; }
|
|
790
|
+
.fx-perm-link { color: var(--fe-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; text-decoration: none; }
|
|
791
|
+
.fx-perm-link:hover { text-decoration: underline; }
|
|
792
|
+
.fx-perm-pin { margin: 8px 0 0; font-size: 13px; color: var(--fe-text); display: flex; align-items: center; gap: 8px; }
|
|
793
|
+
.fx-perm-pin code, .fx-perm-reveal code { font-family: var(--fe-font-mono, monospace); background: var(--fe-bg-hover); padding: 1px 6px; border-radius: 5px; }
|
|
794
|
+
.fx-perm-mailrow { display: flex; gap: 8px; margin-top: 10px; }
|
|
795
|
+
.fx-perm-mailrow .fx-perm-input { flex: 1; }
|
|
796
|
+
.fx-perm-hint { font-size: 13px; color: var(--fe-text-muted); margin: 10px 0 0; }
|
|
797
|
+
|
|
798
|
+
/* file-drop tab */
|
|
799
|
+
.fx-perm-dropintro { margin: 0 0 12px; line-height: 1.4; }
|
|
800
|
+
.fx-perm-advtoggle { margin-top: 10px; padding-left: 2px; }
|
|
801
|
+
.fx-perm-adv {
|
|
802
|
+
margin-top: 8px; padding-top: 10px; border-top: 1px dashed var(--fe-border);
|
|
803
|
+
display: flex; flex-direction: column; gap: 8px;
|
|
804
|
+
}
|
|
805
|
+
.fx-perm-adv-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; }
|
|
806
|
+
.fx-perm-input--sm { width: 130px; padding: 5px 8px; font-size: 12px; }
|
|
807
|
+
|
|
808
|
+
.fx-perm-warn {
|
|
809
|
+
background: rgba(245, 158, 11, 0.14); border: 1px solid rgba(245, 158, 11, 0.4);
|
|
810
|
+
border-radius: var(--fe-radius-sm, 7px); padding: 8px 10px; font-size: 13px; margin-bottom: 10px; color: var(--fe-text);
|
|
811
|
+
}
|
|
812
|
+
.fx-perm-notice { margin-top: 8px; font-size: 13px; color: var(--fe-text-muted); }
|
|
813
|
+
.fx-perm-reveal { margin-top: 8px; font-size: 13px; word-break: break-all; color: var(--fe-text); }
|
|
814
|
+
.fx-perm-muted { color: var(--fe-text-muted); font-size: 13px; }
|
|
815
|
+
</style>
|