@brftech/filex-core 0.40.0 → 0.41.1
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/README.md +32 -25
- package/dist/ArchiveViewer-DxxjhYZD.js +124 -0
- package/dist/ArchiveViewer-DxxjhYZD.js.map +1 -0
- package/dist/CsvViewer-CyuwENI_.js +141 -0
- package/dist/CsvViewer-CyuwENI_.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-BIYsTX97.js} +48 -42
- package/dist/DrawioViewer-BIYsTX97.js.map +1 -0
- package/dist/EpubViewer-K9PjCMmS.js +163 -0
- package/dist/EpubViewer-K9PjCMmS.js.map +1 -0
- package/dist/IpynbViewer-DYU6Zgz2.js +184 -0
- package/dist/IpynbViewer-DYU6Zgz2.js.map +1 -0
- package/dist/MermaidViewer-Bp73aId8.js +137 -0
- package/dist/MermaidViewer-Bp73aId8.js.map +1 -0
- package/dist/PsdViewer-C2eLivw1.js +122 -0
- package/dist/PsdViewer-C2eLivw1.js.map +1 -0
- package/dist/TiffViewer-DsHg5gcz.js +142 -0
- package/dist/TiffViewer-DsHg5gcz.js.map +1 -0
- package/dist/Viewer3D-C_dc2HN9.js +75 -0
- package/dist/Viewer3D-C_dc2HN9.js.map +1 -0
- package/dist/filex-core.js +218 -20361
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +61 -62
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-vxMZpYb-.js +26700 -0
- package/dist/index-vxMZpYb-.js.map +1 -0
- package/dist/index.d.ts +4212 -862
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2154 -506
- package/src/components/AdvancedSearch.vue +870 -0
- package/src/components/Breadcrumb.vue +191 -19
- package/src/components/CommandPalette.vue +66 -22
- package/src/components/ConnectionsPanel.vue +6 -2
- package/src/components/ContextMenu.vue +39 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +951 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +172 -56
- package/src/components/GridView.vue +331 -82
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1173 -194
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +26 -1
- package/src/components/RecentlyOpened.vue +17 -2
- package/src/components/S3KeysPanel.vue +7 -4
- package/src/components/SSHKeysPanel.vue +7 -4
- package/src/components/ShortcutSettings.vue +6 -2
- package/src/components/ShortcutsHelp.vue +4 -1
- package/src/components/SideNav.vue +206 -140
- package/src/components/StorageFields.vue +19 -1
- package/src/components/TabBar.vue +87 -1
- package/src/components/TagPicker.vue +77 -6
- package/src/components/ThemeGallery.vue +13 -198
- package/src/components/ThemePalette.vue +244 -0
- package/src/components/TimeZoneDialog.vue +135 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +35 -14
- package/src/components/Toolbar.vue +953 -465
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +89 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useLocale.ts +382 -26
- package/src/composables/useRowTouch.ts +95 -0
- package/src/index.ts +177 -3
- package/src/lib/actionIcons.ts +330 -0
- package/src/lib/advSearch.ts +162 -0
- package/src/lib/dateGroups.ts +197 -0
- package/src/lib/destinationTree.ts +235 -0
- package/src/lib/downloadSelection.ts +154 -0
- package/src/lib/fileFilters.ts +319 -12
- package/src/lib/fileIcons.ts +299 -5
- package/src/lib/filePreview.ts +605 -0
- package/src/lib/listing.ts +72 -0
- package/src/lib/shareTtl.ts +31 -2
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +151 -116
- package/src/lib/timezone.ts +501 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +416 -26
- package/src/locales/tr.ts +409 -26
- package/src/modals/ConvertModal.vue +23 -3
- package/src/modals/DestinationPickerModal.vue +341 -0
- package/src/modals/Modal.vue +18 -4
- package/src/modals/NewDocumentModal.vue +525 -0
- package/src/modals/NewFolderModal.vue +15 -9
- package/src/modals/PermissionsModal.vue +557 -476
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5409 -8
- package/src/styles/variables.css +139 -65
- package/src/types/Connections.ts +9 -0
- package/src/types/ExplorerConfig.ts +144 -39
- package/src/types/FileNode.ts +28 -0
- package/src/viewers/ArchiveViewer.vue +46 -8
- package/src/viewers/CsvViewer.vue +19 -2
- package/src/viewers/DrawioViewer.vue +12 -1
- package/src/viewers/EpubViewer.vue +50 -6
- package/src/viewers/IpynbViewer.vue +19 -2
- package/src/viewers/MermaidViewer.vue +20 -3
- package/src/viewers/PdfViewer.vue +24 -4
- package/src/viewers/PsdViewer.vue +28 -4
- package/src/viewers/TiffViewer.vue +19 -2
- package/src/viewers/Viewer3D.vue +36 -2
- package/dist/ArchiveViewer-Cdfv9LFC.js +0 -99
- package/dist/ArchiveViewer-Cdfv9LFC.js.map +0 -1
- package/dist/CsvViewer-CqWeV8VO.js +0 -131
- package/dist/CsvViewer-CqWeV8VO.js.map +0 -1
- package/dist/DrawioViewer-BNALOB04.js.map +0 -1
- package/dist/EpubViewer-BPmlImG7.js +0 -145
- package/dist/EpubViewer-BPmlImG7.js.map +0 -1
- package/dist/IpynbViewer-CDlQmuCV.js +0 -175
- package/dist/IpynbViewer-CDlQmuCV.js.map +0 -1
- package/dist/MermaidViewer-sfZWdR-d.js +0 -127
- package/dist/MermaidViewer-sfZWdR-d.js.map +0 -1
- package/dist/PsdViewer-DwuySZsb.js +0 -112
- package/dist/PsdViewer-DwuySZsb.js.map +0 -1
- package/dist/TiffViewer-BDQ3d3CQ.js +0 -132
- package/dist/TiffViewer-BDQ3d3CQ.js.map +0 -1
- package/dist/Viewer3D-CJJJgkuv.js +0 -60
- package/dist/Viewer3D-CJJJgkuv.js.map +0 -1
- package/src/components/SecondaryPane.vue +0 -447
- package/src/modals/ShareModal.vue +0 -139
|
@@ -1,15 +1,49 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Access modal — "Share / Permissions", the popup behind the explorer's
|
|
4
|
+
* `access` action.
|
|
5
|
+
*
|
|
6
|
+
* gorunum:v2-share — the LOOK changed, the capabilities did not.
|
|
7
|
+
*
|
|
8
|
+
* It used to open as three peer tabs (People · Link · Request files), each a
|
|
9
|
+
* grid of controls competing for the first glance. Nine times in ten the
|
|
10
|
+
* person who opened it wants one thing: turn the link on and copy it. So the
|
|
11
|
+
* dialog now leads with exactly that — one switch, one plain sentence saying
|
|
12
|
+
* who can open the item right now, and the link with its Copy button — and
|
|
13
|
+
* everything else sits underneath in a **named** second tier:
|
|
14
|
+
*
|
|
15
|
+
* · "Bağlantı seçenekleri" — PIN, expiry, download cap, the one-line curl,
|
|
16
|
+
* e-mail delivery, the OS share sheet, and every existing download link.
|
|
17
|
+
* · "Erişimi olanlar" — the per-user/per-group grants (owner only).
|
|
18
|
+
* · "Dosya İste" — the inbound drop link (folders only).
|
|
19
|
+
*
|
|
20
|
+
* ⚠ Second tier, not "Advanced": each section says what is in it and carries a
|
|
21
|
+
* one-glance summary of its own state ("PIN · 7 gün · 3 indirme"), because a
|
|
22
|
+
* control nobody can find is a control that does not exist. `initialTab` still
|
|
23
|
+
* decides which section opens with the dialog, so "Request files" still lands
|
|
24
|
+
* on the drop link.
|
|
25
|
+
*
|
|
26
|
+
* ⚠ No `<style>` block: the CSS lives in `styles/base.css`
|
|
27
|
+
* (`gorunum:v2-share`). A scoped style block here is silently dropped in the
|
|
28
|
+
* web-component build — see web/tests/api/scopedStyles.test.ts.
|
|
29
|
+
*/
|
|
30
|
+
import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue';
|
|
8
31
|
import type { FileApi, Grant, UserSuggestion } from '../composables/useFileApi';
|
|
9
32
|
import type { ShareInfo } from '../types/FileNode';
|
|
10
33
|
import { shareCliCommand } from '../lib/shareCli';
|
|
11
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
STOCK_EXPIRY_DAYS,
|
|
36
|
+
clampExpiryOptions,
|
|
37
|
+
defaultExpiryDays,
|
|
38
|
+
shareDetailLine,
|
|
39
|
+
ttlCeilingHint,
|
|
40
|
+
validUntilLine,
|
|
41
|
+
} from '../lib/shareTtl';
|
|
42
|
+
import { EXPLORER_CLOCK } from '../lib/timezone';
|
|
12
43
|
import { resolveLocale } from '../locales/resolve';
|
|
44
|
+
import { formatByteSize, useLocale } from '../composables/useLocale';
|
|
45
|
+
import { actionIconSvg } from '../lib/actionIcons';
|
|
46
|
+
import { fileIconTile } from '../lib/fileIcons';
|
|
13
47
|
|
|
14
48
|
const props = defineProps<{
|
|
15
49
|
api: FileApi;
|
|
@@ -21,23 +55,24 @@ const props = defineProps<{
|
|
|
21
55
|
* undefined/0 = no ceiling. The expiry choices are derived from it. */
|
|
22
56
|
shareMaxTtlDays?: number;
|
|
23
57
|
/**
|
|
24
|
-
* surucu:d1 — which
|
|
25
|
-
* every existing caller has.
|
|
58
|
+
* surucu:d1 — which part of the dialog the caller wants opened. Absent =
|
|
59
|
+
* 'perms', the behaviour every existing caller has.
|
|
26
60
|
*
|
|
27
|
-
* The drive shell's "Request files" needs 'drop': the file-drop link
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* what it does.
|
|
61
|
+
* The drive shell's "Request files" needs 'drop': the file-drop link lives
|
|
62
|
+
* in this dialog, and a menu entry that opened it anywhere else would be a
|
|
63
|
+
* menu entry that lied about what it does.
|
|
31
64
|
*
|
|
32
|
-
* ⚠ A hint, not a lock: `load()` still falls back to
|
|
33
|
-
* turns out not to be an owner, because the
|
|
34
|
-
* see.
|
|
65
|
+
* ⚠ A hint, not a lock: `load()` still falls back to the link tier when the
|
|
66
|
+
* caller turns out not to be an owner, because the grants section is not
|
|
67
|
+
* theirs to see.
|
|
35
68
|
*/
|
|
36
69
|
initialTab?: 'perms' | 'share' | 'drop';
|
|
37
70
|
}>();
|
|
38
71
|
const emit = defineEmits<{ (e: 'close'): void }>();
|
|
39
72
|
|
|
40
|
-
const
|
|
73
|
+
const localeCode = computed(() => resolveLocale(props.locale));
|
|
74
|
+
const { t } = useLocale(() => localeCode.value);
|
|
75
|
+
const tr = computed(() => localeCode.value !== 'en');
|
|
41
76
|
function L(t: string, e: string): string {
|
|
42
77
|
return tr.value ? t : e;
|
|
43
78
|
}
|
|
@@ -50,10 +85,23 @@ const pathParts = computed(() => {
|
|
|
50
85
|
const segs = rel.split('/').filter(Boolean);
|
|
51
86
|
return { adapter, name: segs.length ? segs[segs.length - 1] : adapter, rel };
|
|
52
87
|
});
|
|
88
|
+
// The type tile beside the title — the same badge the listing draws, so the
|
|
89
|
+
// dialog is visibly about the row the user right-clicked.
|
|
90
|
+
const titleTile = computed(() => {
|
|
91
|
+
const name = pathParts.value.name;
|
|
92
|
+
const dot = name.lastIndexOf('.');
|
|
93
|
+
return fileIconTile({
|
|
94
|
+
type: props.isDir ? 'dir' : 'file',
|
|
95
|
+
extension: !props.isDir && dot > 0 ? name.slice(dot + 1) : '',
|
|
96
|
+
});
|
|
97
|
+
});
|
|
53
98
|
|
|
54
|
-
type
|
|
55
|
-
const
|
|
56
|
-
|
|
99
|
+
type Section = 'link' | 'people' | 'drop';
|
|
100
|
+
const open = ref<Record<Section, boolean>>({ link: false, people: false, drop: false });
|
|
101
|
+
function toggleSection(s: Section) {
|
|
102
|
+
open.value = { ...open.value, [s]: !open.value[s] };
|
|
103
|
+
}
|
|
104
|
+
const canManage = ref(false); // owner/admin → can see the grants section
|
|
57
105
|
|
|
58
106
|
// ── permissions state ──
|
|
59
107
|
const loading = ref(true);
|
|
@@ -94,6 +142,23 @@ const shareMailTo = ref('');
|
|
|
94
142
|
const shareMailBusy = ref(false);
|
|
95
143
|
const shareMailNotice = ref('');
|
|
96
144
|
|
|
145
|
+
/**
|
|
146
|
+
* ⚠ `listShares` returns BOTH kinds of link — a download link (`/s/…`) and a
|
|
147
|
+
* file-drop link (`/d/…`) are the same row with a different `kind`. They used
|
|
148
|
+
* to be listed together under "Existing links", which meant an upload link
|
|
149
|
+
* appeared in the download tab and the "link sharing" state could not be read
|
|
150
|
+
* off the list at all. Split at the source: each section owns its own kind.
|
|
151
|
+
*
|
|
152
|
+
* `kind` is absent from the `ShareInfo` type but present on the wire (see
|
|
153
|
+
* backend/internal/api/handlers/share.go), so it is read through a narrow cast
|
|
154
|
+
* rather than by widening a shared type this dialog does not own.
|
|
155
|
+
*/
|
|
156
|
+
function shareKind(s: ShareInfo): string {
|
|
157
|
+
return (s as ShareInfo & { kind?: string }).kind ?? 'download';
|
|
158
|
+
}
|
|
159
|
+
const downloadShares = computed(() => shares.value.filter((s) => shareKind(s) !== 'drop'));
|
|
160
|
+
const dropShares = computed(() => shares.value.filter((s) => shareKind(s) === 'drop'));
|
|
161
|
+
|
|
97
162
|
// ⚠ Derived from the server's ceiling, never a fixed list: offering "30 days"
|
|
98
163
|
// on a server that keeps links for 7 would show a choice that is not one.
|
|
99
164
|
// The same helper drives every surface (see lib/shareTtl.ts).
|
|
@@ -195,14 +260,15 @@ async function reload() {
|
|
|
195
260
|
storageRbac.value = r.storage_rbac;
|
|
196
261
|
canManage.value = true;
|
|
197
262
|
} catch (e) {
|
|
198
|
-
// 403 = caller is editor (not owner): no
|
|
263
|
+
// 403 = caller is editor (not owner): no grants section, link only.
|
|
199
264
|
const st = (e as { status?: number }).status;
|
|
200
265
|
if (st === 403) {
|
|
201
266
|
canManage.value = false;
|
|
202
|
-
// Editor (not owner):
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
|
|
267
|
+
// Editor (not owner): the grants section is not theirs — close it and
|
|
268
|
+
// leave the link tier, which is the whole dialog for them.
|
|
269
|
+
// ⚠ Unless the caller asked for the drop link, which an editor may
|
|
270
|
+
// perfectly well mint.
|
|
271
|
+
open.value = { ...open.value, people: false };
|
|
206
272
|
} else {
|
|
207
273
|
err.value = e instanceof Error ? e.message : String(e);
|
|
208
274
|
}
|
|
@@ -218,12 +284,29 @@ async function reloadShares() {
|
|
|
218
284
|
shares.value = [];
|
|
219
285
|
}
|
|
220
286
|
}
|
|
287
|
+
function onKey(e: KeyboardEvent) {
|
|
288
|
+
if (e.key === 'Escape') emit('close');
|
|
289
|
+
}
|
|
221
290
|
onMounted(async () => {
|
|
291
|
+
// surucu:d1 — the caller's entry point decides which section opens with the
|
|
292
|
+
// dialog.
|
|
293
|
+
//
|
|
294
|
+
// ⚠ The DEFAULT (no `initialTab`, i.e. the plain "Share / Permissions"
|
|
295
|
+
// action) now opens NOTHING. It used to land on the permissions tab, which
|
|
296
|
+
// meant the common case — turn the link on, copy it — arrived buried under a
|
|
297
|
+
// grant editor. The link tier is the answer to the default question; the
|
|
298
|
+
// grants are one click away with their own state ("2 people") readable from
|
|
299
|
+
// the closed heading. A caller that explicitly wants the grants still says
|
|
300
|
+
// so, and "Request files" still lands on the drop link.
|
|
301
|
+
if (props.initialTab === 'drop') open.value = { ...open.value, drop: true };
|
|
302
|
+
else if (props.initialTab === 'perms') open.value = { ...open.value, people: true };
|
|
303
|
+
document.addEventListener('keydown', onKey);
|
|
222
304
|
await reload();
|
|
223
305
|
await reloadShares();
|
|
224
306
|
});
|
|
225
307
|
onBeforeUnmount(() => {
|
|
226
308
|
if (searchTimer) clearTimeout(searchTimer);
|
|
309
|
+
document.removeEventListener('keydown', onKey);
|
|
227
310
|
});
|
|
228
311
|
|
|
229
312
|
function onEmailInput() {
|
|
@@ -315,13 +398,14 @@ async function inviteCreateUser() {
|
|
|
315
398
|
busy.value = false;
|
|
316
399
|
}
|
|
317
400
|
}
|
|
318
|
-
// "Just send a share link" →
|
|
319
|
-
// owner creates a link (with their chosen expiry/PIN) and mails it
|
|
401
|
+
// "Just send a share link" → open the link options with the address prefilled
|
|
402
|
+
// so the owner creates a link (with their chosen expiry/PIN) and mails it
|
|
403
|
+
// from there.
|
|
320
404
|
function gotoShareWithMail() {
|
|
321
405
|
shareMailTo.value = email.value.trim().toLowerCase();
|
|
322
406
|
noAccount.value = false;
|
|
323
407
|
notice.value = '';
|
|
324
|
-
|
|
408
|
+
open.value = { ...open.value, link: true };
|
|
325
409
|
}
|
|
326
410
|
async function changeLevel(g: Grant, newLevel: string) {
|
|
327
411
|
if (newLevel === g.level) return;
|
|
@@ -375,6 +459,39 @@ async function createLink() {
|
|
|
375
459
|
shareBusy.value = false;
|
|
376
460
|
}
|
|
377
461
|
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* gorunum:v2-share — the switch.
|
|
465
|
+
*
|
|
466
|
+
* ON with nothing minted mints one with whatever the options tier currently
|
|
467
|
+
* says (that is the default: the server's ceiling as the expiry, no PIN, no
|
|
468
|
+
* cap). OFF revokes every DOWNLOAD link on this item, because that is what
|
|
469
|
+
* "link sharing is off" has to mean — a switch that left a live link behind
|
|
470
|
+
* would be the most dangerous control in the dialog.
|
|
471
|
+
*
|
|
472
|
+
* ⚠ Drop links are deliberately untouched: they are an inbound door managed by
|
|
473
|
+
* its own section, and revoking someone's upload invitation because the owner
|
|
474
|
+
* turned off downloads is not what either control says.
|
|
475
|
+
*/
|
|
476
|
+
async function toggleLink() {
|
|
477
|
+
if (shareBusy.value) return;
|
|
478
|
+
if (linkOn.value) {
|
|
479
|
+
shareBusy.value = true;
|
|
480
|
+
shareErr.value = '';
|
|
481
|
+
try {
|
|
482
|
+
for (const s of downloadShares.value) await props.api.revokeShare(s.uuid);
|
|
483
|
+
shareResult.value = null;
|
|
484
|
+
await reloadShares();
|
|
485
|
+
} catch (e) {
|
|
486
|
+
shareErr.value = e instanceof Error ? e.message : String(e);
|
|
487
|
+
} finally {
|
|
488
|
+
shareBusy.value = false;
|
|
489
|
+
}
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
await createLink();
|
|
493
|
+
}
|
|
494
|
+
|
|
378
495
|
async function sendShareMail() {
|
|
379
496
|
const list = splitEmails(shareMailTo.value);
|
|
380
497
|
if (!list.length) {
|
|
@@ -437,6 +554,7 @@ async function createDropLink() {
|
|
|
437
554
|
expiresAt: r.share.expires_at ?? null,
|
|
438
555
|
clamped: !!r.share.expiry_clamped,
|
|
439
556
|
};
|
|
557
|
+
await reloadShares();
|
|
440
558
|
} catch (e) {
|
|
441
559
|
dropErr.value = e instanceof Error ? e.message : String(e);
|
|
442
560
|
} finally {
|
|
@@ -475,8 +593,15 @@ async function sendDropMail() {
|
|
|
475
593
|
}
|
|
476
594
|
async function revoke(s: ShareInfo) {
|
|
477
595
|
shareBusy.value = true;
|
|
478
|
-
try {
|
|
479
|
-
|
|
596
|
+
try {
|
|
597
|
+
await props.api.revokeShare(s.uuid);
|
|
598
|
+
// ⚠ The lead tier reads `shareResult` first. Revoking the link that is
|
|
599
|
+
// showing up there has to clear it too, or the dialog goes on offering a
|
|
600
|
+
// Copy button — and a switch reading ON — for a link the server has just
|
|
601
|
+
// thrown away.
|
|
602
|
+
if (shareResult.value && shareResult.value.url === s.url) shareResult.value = null;
|
|
603
|
+
await reloadShares();
|
|
604
|
+
} catch (e) { shareErr.value = e instanceof Error ? e.message : String(e); }
|
|
480
605
|
finally { shareBusy.value = false; }
|
|
481
606
|
}
|
|
482
607
|
function copy(text: string, tag = 'url') {
|
|
@@ -493,15 +618,24 @@ function copy(text: string, tag = 'url') {
|
|
|
493
618
|
// same as an emailed link.
|
|
494
619
|
const canShare = computed(() => typeof navigator !== 'undefined' && typeof navigator.share === 'function');
|
|
495
620
|
|
|
496
|
-
|
|
621
|
+
/**
|
|
622
|
+
* The size line in the share message, spelled the way the SERVER spells it.
|
|
623
|
+
*
|
|
624
|
+
* ⚠ This one really does have to mirror `humanSize()` in
|
|
625
|
+
* backend/internal/api/handlers/mail_templates.go — the same file forwarded
|
|
626
|
+
* by e-mail and by the OS share sheet must not carry two different sizes. So
|
|
627
|
+
* the mirroring is stated as arguments to the one formatter (1024, always one
|
|
628
|
+
* decimal, a dot and English letters, because that is what Go's `%.1f %cB`
|
|
629
|
+
* prints) instead of being a fifth private copy of the arithmetic.
|
|
630
|
+
*
|
|
631
|
+
* ⚠ It is therefore the one place in the UI that is NOT decimal, so a share
|
|
632
|
+
* message reads "1.4 MB" where the listing row reads "1.5 MB". The register
|
|
633
|
+
* entry for this was right about the real fix: the e-mail should render from
|
|
634
|
+
* one source rather than the UI copying its rounding.
|
|
635
|
+
*/
|
|
497
636
|
function humanSize(b?: number): string {
|
|
498
637
|
if (!b || b <= 0) return '';
|
|
499
|
-
|
|
500
|
-
if (b < unit) return `${b} B`;
|
|
501
|
-
const units = ['K', 'M', 'G', 'T', 'P', 'E'];
|
|
502
|
-
let div = unit, exp = 0;
|
|
503
|
-
for (let n = b / unit; n >= unit; n /= unit) { div *= unit; exp++; }
|
|
504
|
-
return `${(b / div).toFixed(1)} ${units[exp]}B`;
|
|
638
|
+
return formatByteSize(b, { base: 1024, digits: 'fixed1', numberLocale: null });
|
|
505
639
|
}
|
|
506
640
|
|
|
507
641
|
function expiryLine(days: number): string {
|
|
@@ -510,10 +644,67 @@ function expiryLine(days: number): string {
|
|
|
510
644
|
}
|
|
511
645
|
// What the server actually stored — shown under a fresh link so the real
|
|
512
646
|
// expiry is visible even when the server shortened the request.
|
|
647
|
+
// The explorer this dialog belongs to reads deadlines on its own clock.
|
|
648
|
+
const clock = inject(EXPLORER_CLOCK, undefined);
|
|
649
|
+
|
|
513
650
|
function validUntil(r: { expiresAt?: string | null } | null): string {
|
|
514
|
-
return validUntilLine(r?.expiresAt ?? null, tr.value ? 'tr' : 'en');
|
|
651
|
+
return validUntilLine(r?.expiresAt ?? null, tr.value ? 'tr' : 'en', clock);
|
|
515
652
|
}
|
|
516
653
|
|
|
654
|
+
/* ── the top tier: one switch, one sentence, one link ──────────────────── */
|
|
655
|
+
|
|
656
|
+
/** The link the header row shows: the one just minted, else the oldest live one. */
|
|
657
|
+
const primaryLink = computed(() => shareResult.value?.url ?? downloadShares.value[0]?.url ?? '');
|
|
658
|
+
const linkOn = computed(() => !!primaryLink.value);
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* The plain sentence. It says who can open the item RIGHT NOW, which is the
|
|
662
|
+
* question the dialog exists to answer.
|
|
663
|
+
*
|
|
664
|
+
* ⚠ The PIN claim is only made about a link this dialog minted: `listShares`
|
|
665
|
+
* does not return `password_pin` (by design — the server will not re-serve a
|
|
666
|
+
* PIN), so for a link that was already there we say how many exist rather than
|
|
667
|
+
* guessing at its protection. A confident wrong sentence about who can read a
|
|
668
|
+
* file is worse than a vaguer true one.
|
|
669
|
+
*/
|
|
670
|
+
const whoLine = computed(() => {
|
|
671
|
+
if (!linkOn.value) return t('access.who.private');
|
|
672
|
+
if (shareResult.value) return shareResult.value.pin ? t('access.who.pin') : t('access.who.link');
|
|
673
|
+
return t('access.who.existing', { n: downloadShares.value.length });
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
/** The muted second line under it: what the server actually stored. */
|
|
677
|
+
const linkDetail = computed(() => {
|
|
678
|
+
const bits: string[] = [];
|
|
679
|
+
if (shareResult.value) {
|
|
680
|
+
bits.push(validUntil(shareResult.value));
|
|
681
|
+
if (shareResult.value.clamped) bits.push(L('(sunucu sınırı uygulandı)', '(server limit applied)'));
|
|
682
|
+
if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
|
|
683
|
+
} else if (downloadShares.value[0]) {
|
|
684
|
+
const s = downloadShares.value[0];
|
|
685
|
+
bits.push(validUntil({ expiresAt: s.expires_at ?? null }));
|
|
686
|
+
if (s.max_downloads) bits.push(maxDlOptions.find((o) => o.v === s.max_downloads)?.l ?? String(s.max_downloads));
|
|
687
|
+
}
|
|
688
|
+
return shareDetailLine(bits);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
/* Section summaries — a named section still has to say what is inside it, or
|
|
692
|
+
the reader has to open all three to find the control they came for. */
|
|
693
|
+
const linkSummary = computed(() => {
|
|
694
|
+
const bits: string[] = [];
|
|
695
|
+
bits.push(sharePwd.value ? t('access.sum.pin_on') : t('access.sum.pin_off'));
|
|
696
|
+
bits.push(expiryLabel(shareExpiry.value));
|
|
697
|
+
if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
|
|
698
|
+
return bits.filter(Boolean).join(' · ');
|
|
699
|
+
});
|
|
700
|
+
const peopleSummary = computed(() => {
|
|
701
|
+
const n = direct.value.length + inherited.value.length;
|
|
702
|
+
return n ? t('access.sum.people', { n }) : t('access.sum.people_none');
|
|
703
|
+
});
|
|
704
|
+
const dropSummary = computed(() =>
|
|
705
|
+
dropShares.value.length ? t('access.sum.drop', { n: dropShares.value.length }) : t('access.sum.drop_none'),
|
|
706
|
+
);
|
|
707
|
+
|
|
517
708
|
// Text + title for a download-share link, mirroring shareMailText().
|
|
518
709
|
function shareBody(): { title: string; text: string } {
|
|
519
710
|
const name = pathParts.value.name;
|
|
@@ -573,478 +764,368 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
573
764
|
</script>
|
|
574
765
|
|
|
575
766
|
<template>
|
|
576
|
-
<div class="fx-perm-overlay" @click.self="emit('close')">
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
</
|
|
767
|
+
<div class="fx-perm-overlay fe-share__scrim" @click.self="emit('close')">
|
|
768
|
+
<!-- ⚠ `fx-perm-modal` is kept alongside the new class: e2e/shots/capture.mjs
|
|
769
|
+
and the embedders' own harnesses address the dialog by it. -->
|
|
770
|
+
<div class="fx-perm-modal fe-share" role="dialog" aria-modal="true" :aria-label="t('access.title', { name: pathParts.name })">
|
|
771
|
+
<header class="fe-share__head">
|
|
772
|
+
<span class="fe-share__tile" aria-hidden="true" v-html="titleTile"></span>
|
|
773
|
+
<div class="fe-share__headtext">
|
|
774
|
+
<h3 class="fe-share__title">{{ t('access.title', { name: pathParts.name }) }}</h3>
|
|
775
|
+
<span class="fe-share__path" :title="path">{{ pathParts.adapter }}</span>
|
|
585
776
|
</div>
|
|
586
|
-
<button class="
|
|
777
|
+
<button type="button" class="fe-share__close" :aria-label="t('access.close')" :title="t('access.close')" @click="emit('close')">
|
|
778
|
+
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
779
|
+
</button>
|
|
587
780
|
</header>
|
|
588
781
|
|
|
589
|
-
<div class="
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
class="
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
@click="tab = 'drop'"
|
|
606
|
-
>{{ L('Dosya İste', 'Request files') }}</button>
|
|
607
|
-
</div>
|
|
608
|
-
|
|
609
|
-
<div class="fx-perm-body">
|
|
610
|
-
<!-- ───────── Permissions tab ───────── -->
|
|
611
|
-
<template v-if="tab === 'perms' && canManage">
|
|
612
|
-
<div v-if="!storageRbac" class="fx-perm-warn">
|
|
613
|
-
{{ 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.') }}
|
|
782
|
+
<div class="fe-share__body">
|
|
783
|
+
<!-- ───────── tier 1: the link, the whole reason people open this ───── -->
|
|
784
|
+
<div class="fe-share__lead">
|
|
785
|
+
<div class="fe-share__switchrow">
|
|
786
|
+
<span class="fe-share__leadicon" aria-hidden="true" v-html="actionIconSvg('access')"></span>
|
|
787
|
+
<span class="fe-share__leadlabel">{{ t('access.link.switch') }}</span>
|
|
788
|
+
<button
|
|
789
|
+
type="button"
|
|
790
|
+
class="fe-share__switch"
|
|
791
|
+
role="switch"
|
|
792
|
+
:aria-checked="linkOn ? 'true' : 'false'"
|
|
793
|
+
:aria-label="t('access.link.switch')"
|
|
794
|
+
data-testid="share-switch"
|
|
795
|
+
:disabled="shareBusy"
|
|
796
|
+
@click="toggleLink"
|
|
797
|
+
><span class="fe-share__knob"></span></button>
|
|
614
798
|
</div>
|
|
615
|
-
<
|
|
616
|
-
|
|
617
|
-
<template v-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
<
|
|
621
|
-
<
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
799
|
+
<p class="fe-share__who" data-testid="share-who">{{ whoLine }}</p>
|
|
800
|
+
|
|
801
|
+
<template v-if="linkOn">
|
|
802
|
+
<div class="fe-share__linkrow">
|
|
803
|
+
<a :href="primaryLink" target="_blank" rel="noopener" class="fe-share__url" :title="primaryLink">{{ primaryLink }}</a>
|
|
804
|
+
<button type="button" class="fe-share__copy" data-testid="share-copy" @click="copy(primaryLink, 'new')">
|
|
805
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
806
|
+
<span>{{ copied === 'new' ? t('access.copied') : t('access.copy') }}</span>
|
|
807
|
+
</button>
|
|
808
|
+
</div>
|
|
809
|
+
<p v-if="linkDetail" class="fe-share__detail" data-testid="share-valid-until">{{ linkDetail }}</p>
|
|
810
|
+
<div v-if="shareResult?.pin" class="fe-share__pinrow">
|
|
811
|
+
<span class="fe-share__pinlabel">PIN</span>
|
|
812
|
+
<code class="fe-share__pin">{{ shareResult?.pin }}</code>
|
|
813
|
+
<button type="button" class="fe-share__mini" @click="copy(shareResult?.pin ?? '', 'sharepin')">
|
|
814
|
+
{{ copied === 'sharepin' ? t('access.copied') : t('access.copy') }}
|
|
815
|
+
</button>
|
|
816
|
+
</div>
|
|
817
|
+
</template>
|
|
818
|
+
<div v-if="shareErr" class="fe-share__warn">{{ shareErr }}</div>
|
|
819
|
+
</div>
|
|
820
|
+
|
|
821
|
+
<!-- ───────── tier 2: named sections, one click each ───────────────── -->
|
|
822
|
+
<div class="fe-share__sections">
|
|
823
|
+
<!-- Link options: PIN · expiry · download cap · curl · e-mail · links -->
|
|
824
|
+
<section class="fe-share__section">
|
|
825
|
+
<button
|
|
826
|
+
type="button"
|
|
827
|
+
class="fe-share__sechead"
|
|
828
|
+
:aria-expanded="open.link ? 'true' : 'false'"
|
|
829
|
+
data-testid="share-options-toggle"
|
|
830
|
+
@click="toggleSection('link')"
|
|
831
|
+
>
|
|
832
|
+
<svg class="fe-share__caret" :class="{ 'is-open': open.link }" viewBox="0 0 24 24" fill="none"
|
|
833
|
+
stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
|
|
834
|
+
aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
|
|
835
|
+
<span class="fe-share__seclabel">{{ t('access.section.link') }}</span>
|
|
836
|
+
<span class="fe-share__secmeta">{{ linkSummary }}</span>
|
|
837
|
+
</button>
|
|
838
|
+
<div v-if="open.link" class="fe-share__panel">
|
|
839
|
+
<div class="fe-share__opts">
|
|
840
|
+
<label class="fe-share__check">
|
|
841
|
+
<input type="checkbox" v-model="sharePwd" />
|
|
842
|
+
<span>{{ L('PIN ile koru', 'Protect with a PIN') }}</span>
|
|
843
|
+
</label>
|
|
844
|
+
<label class="fe-share__field">
|
|
845
|
+
<span class="fe-share__fieldlabel">{{ L('Süre', 'Expiry') }}</span>
|
|
846
|
+
<select v-model.number="shareExpiry" class="fe-share__select" data-testid="share-expiry">
|
|
847
|
+
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
848
|
+
</select>
|
|
849
|
+
</label>
|
|
850
|
+
<label class="fe-share__field">
|
|
851
|
+
<span class="fe-share__fieldlabel">{{ L('İndirme limiti', 'Download limit') }}</span>
|
|
852
|
+
<select v-model.number="shareMaxDl" class="fe-share__select">
|
|
853
|
+
<option v-for="o in maxDlOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
854
|
+
</select>
|
|
855
|
+
</label>
|
|
856
|
+
</div>
|
|
857
|
+
<p v-if="ttlHint" class="fe-share__hint" data-testid="share-ttl-hint">{{ ttlHint }}</p>
|
|
858
|
+
<button type="button" class="fx-perm-create fe-share__btn fe-share__btn--primary fe-share__btn--wide" :disabled="shareBusy" @click="createLink">
|
|
859
|
+
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
860
|
+
</button>
|
|
861
|
+
|
|
862
|
+
<p v-if="!shareResult && shareMailTo" class="fe-share__hint">
|
|
863
|
+
{{ L('Bir bağlantı oluşturun, ardından', 'Create a link, then it will be sent to') }}
|
|
864
|
+
<strong>{{ shareMailTo }}</strong> {{ L('adresine gönderin.', '.') }}
|
|
865
|
+
</p>
|
|
866
|
+
|
|
867
|
+
<template v-if="shareResult">
|
|
868
|
+
<!-- one-line download command, for pulling the file onto a server -->
|
|
869
|
+
<div class="fx-perm-cli fe-share__cli">
|
|
870
|
+
<span class="fe-share__fieldlabel">{{ L('Komut satırı', 'Command line') }}</span>
|
|
871
|
+
<div class="fe-share__clirow">
|
|
872
|
+
<code class="fe-share__clicmd" :title="shareCli">{{ shareCli }}</code>
|
|
873
|
+
<button type="button" class="fe-share__mini" @click="copy(shareCli, 'sharecli')">
|
|
874
|
+
{{ copied === 'sharecli' ? t('access.copied') : t('access.copy') }}
|
|
875
|
+
</button>
|
|
876
|
+
</div>
|
|
634
877
|
</div>
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
878
|
+
|
|
879
|
+
<!-- send by email (one or more, comma/space separated) -->
|
|
880
|
+
<div class="fe-share__mailrow">
|
|
881
|
+
<input v-model="shareMailTo" type="text" class="fe-share__input" autocomplete="off"
|
|
882
|
+
:placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendShareMail" />
|
|
883
|
+
<button type="button" class="fe-share__btn" :disabled="shareMailBusy" @click="sendShareMail">
|
|
884
|
+
{{ L('Gönder', 'Send') }}
|
|
885
|
+
</button>
|
|
886
|
+
</div>
|
|
887
|
+
<div v-if="shareMailNotice" class="fe-share__notice">{{ shareMailNotice }}</div>
|
|
888
|
+
|
|
889
|
+
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
890
|
+
<button v-if="canShare" type="button" class="fe-share__btn fe-share__btn--wide" @click="nativeShare(shareBody())">
|
|
891
|
+
<span aria-hidden="true" v-html="actionIconSvg('access')"></span>
|
|
892
|
+
<span>{{ L('Paylaş', 'Share') }}</span>
|
|
640
893
|
</button>
|
|
894
|
+
</template>
|
|
895
|
+
|
|
896
|
+
<div class="fe-share__list">
|
|
897
|
+
<h4 class="fe-share__listhead">{{ L('Mevcut bağlantılar', 'Existing links') }}</h4>
|
|
898
|
+
<p v-if="!downloadShares.length" class="fe-share__empty">{{ L('Yok', 'None') }}</p>
|
|
899
|
+
<div v-for="s in downloadShares" :key="s.uuid" class="fe-share__row">
|
|
900
|
+
<span class="fe-share__url" :title="s.url">{{ s.url }}</span>
|
|
901
|
+
<button type="button" class="fe-share__mini" @click="copy(s.url, s.uuid)">
|
|
902
|
+
{{ copied === s.uuid ? t('access.copied') : t('access.copy') }}
|
|
903
|
+
</button>
|
|
904
|
+
<button type="button" class="fe-share__del" :disabled="shareBusy" :title="L('İptal', 'Revoke')"
|
|
905
|
+
:aria-label="L('İptal', 'Revoke')" @click="revoke(s)">
|
|
906
|
+
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
907
|
+
</button>
|
|
908
|
+
</div>
|
|
641
909
|
</div>
|
|
910
|
+
</div>
|
|
911
|
+
</section>
|
|
642
912
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
913
|
+
<!-- People with access: the per-user / inherited grants -->
|
|
914
|
+
<section v-if="canManage" class="fe-share__section">
|
|
915
|
+
<button
|
|
916
|
+
type="button"
|
|
917
|
+
class="fe-share__sechead"
|
|
918
|
+
:aria-expanded="open.people ? 'true' : 'false'"
|
|
919
|
+
data-testid="share-people-toggle"
|
|
920
|
+
@click="toggleSection('people')"
|
|
921
|
+
>
|
|
922
|
+
<svg class="fe-share__caret" :class="{ 'is-open': open.people }" viewBox="0 0 24 24" fill="none"
|
|
923
|
+
stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
|
|
924
|
+
aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
|
|
925
|
+
<span class="fe-share__seclabel">{{ t('access.section.people') }}</span>
|
|
926
|
+
<span class="fe-share__secmeta">{{ peopleSummary }}</span>
|
|
927
|
+
</button>
|
|
928
|
+
<div v-if="open.people" class="fe-share__panel">
|
|
929
|
+
<div v-if="!storageRbac" class="fe-share__warn">
|
|
930
|
+
{{ 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.') }}
|
|
931
|
+
</div>
|
|
932
|
+
<div v-if="err" class="fe-share__warn">{{ err }}</div>
|
|
933
|
+
<p v-if="loading" class="fe-share__hint">{{ L('Yükleniyor…', 'Loading…') }}</p>
|
|
934
|
+
<template v-else>
|
|
935
|
+
<div class="fe-share__add">
|
|
936
|
+
<div class="fe-share__emailwrap">
|
|
937
|
+
<input v-model="email" type="email" class="fe-share__input" autocomplete="off"
|
|
938
|
+
:placeholder="L('İsim veya e-posta', 'Name or email')"
|
|
939
|
+
@input="onEmailInput" @keyup.enter="submitEmail" @focus="onEmailInput" />
|
|
940
|
+
<ul v-if="showSuggest" class="fe-share__suggest">
|
|
941
|
+
<li v-for="u in suggestions" :key="u.id" @mousedown.prevent="pickUser(u)">
|
|
942
|
+
<span class="fe-share__av fe-share__av--sm">{{ (u.display_name || u.email).charAt(0).toUpperCase() }}</span>
|
|
943
|
+
<span class="fe-share__suggesttxt">
|
|
944
|
+
<span class="fe-share__suggestname">{{ u.display_name || u.email }}</span>
|
|
945
|
+
<span class="fe-share__suggestmeta">{{ u.email }} · {{ u.role }}</span>
|
|
946
|
+
</span>
|
|
947
|
+
</li>
|
|
948
|
+
</ul>
|
|
949
|
+
</div>
|
|
950
|
+
<select v-model="level" class="fe-share__select" :title="levels.find(o => o.v === level)?.d">
|
|
951
|
+
<option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
952
|
+
</select>
|
|
953
|
+
<button type="button" class="fe-share__btn fe-share__btn--primary" :disabled="busy" @click="submitEmail">
|
|
954
|
+
{{ L('Ekle', 'Add') }}
|
|
955
|
+
</button>
|
|
956
|
+
</div>
|
|
957
|
+
|
|
958
|
+
<div v-if="noAccount" class="fe-share__invite">
|
|
959
|
+
<p class="fe-share__hint">{{ L('Bu e-postada hesap yok. Ne yapmak istersiniz?', 'No account for this email — what next?') }}</p>
|
|
960
|
+
<div class="fe-share__inviterow">
|
|
961
|
+
<select v-model="createRole" class="fe-share__select">
|
|
648
962
|
<option value="user">{{ L('Kullanıcı', 'User') }}</option>
|
|
649
963
|
<option value="viewer">{{ L('Görüntüleyen', 'Viewer') }}</option>
|
|
650
964
|
</select>
|
|
651
|
-
<button class="
|
|
965
|
+
<button type="button" class="fe-share__btn fe-share__btn--primary" :disabled="busy" @click="inviteCreateUser">
|
|
652
966
|
{{ L('Kullanıcı oluştur + yetki ver', 'Create user + grant') }}
|
|
653
967
|
</button>
|
|
654
968
|
</div>
|
|
655
|
-
<button class="
|
|
969
|
+
<button type="button" class="fe-share__linkbtn" :disabled="busy" @click="gotoShareWithMail">
|
|
656
970
|
{{ L('Sadece paylaşım linki gönder →', 'Just send a share link →') }}
|
|
657
971
|
</button>
|
|
658
972
|
</div>
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
</div>
|
|
664
|
-
</div>
|
|
973
|
+
<div v-if="notice" class="fe-share__notice">{{ notice }}</div>
|
|
974
|
+
<div v-if="inviteResult?.tempPassword" class="fe-share__notice">
|
|
975
|
+
{{ L('Geçici parola:', 'Temp password:') }} <code class="fe-share__pin">{{ inviteResult.tempPassword }}</code>
|
|
976
|
+
</div>
|
|
665
977
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
978
|
+
<div class="fe-share__list">
|
|
979
|
+
<h4 class="fe-share__listhead">{{ L('Erişimi olanlar', 'People with access') }}</h4>
|
|
980
|
+
<p v-if="!direct.length && !inherited.length" class="fe-share__empty">
|
|
981
|
+
{{ L('Henüz kimseyle paylaşılmadı.', 'Not shared with anyone yet.') }}
|
|
982
|
+
</p>
|
|
983
|
+
<div v-for="g in direct" :key="'d' + g.id" class="fe-share__row">
|
|
984
|
+
<span class="fe-share__av">{{ ginitial(g) }}</span>
|
|
985
|
+
<span class="fe-share__person" :title="g.user_email">{{ glabel(g) }}</span>
|
|
986
|
+
<select class="fe-share__select fe-share__select--sm" :value="g.level"
|
|
987
|
+
@change="changeLevel(g, ($event.target as HTMLSelectElement).value)">
|
|
988
|
+
<option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
989
|
+
</select>
|
|
990
|
+
<button type="button" class="fe-share__del" :disabled="busy" :title="L('Kaldır', 'Remove')"
|
|
991
|
+
:aria-label="L('Kaldır', 'Remove')" @click="removeGrant(g)">
|
|
992
|
+
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
993
|
+
</button>
|
|
994
|
+
</div>
|
|
995
|
+
<div v-for="g in inherited" :key="'i' + g.id" class="fe-share__row fe-share__row--dim">
|
|
996
|
+
<span class="fe-share__av fe-share__av--dim">{{ ginitial(g) }}</span>
|
|
997
|
+
<span class="fe-share__person" :title="g.user_email">{{ glabel(g) }}</span>
|
|
998
|
+
<span class="fe-share__badge">{{ levelLabel(g.level) }}</span>
|
|
999
|
+
<span class="fe-share__from" :title="L('Üst klasörden gelir', 'Inherited from') + ': ' + (g.path_prefix || '/')">
|
|
1000
|
+
{{ g.path_prefix || '/' }}
|
|
1001
|
+
</span>
|
|
1002
|
+
</div>
|
|
1003
|
+
</div>
|
|
1004
|
+
</template>
|
|
689
1005
|
</div>
|
|
690
|
-
</
|
|
691
|
-
</template>
|
|
1006
|
+
</section>
|
|
692
1007
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
</
|
|
707
|
-
<label class="fx-perm-field">
|
|
708
|
-
<span class="fx-perm-muted">{{ L('İndirme limiti', 'Download limit') }}</span>
|
|
709
|
-
<select v-model.number="shareMaxDl" class="fx-perm-sel fx-perm-sel--sm">
|
|
710
|
-
<option v-for="o in maxDlOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
711
|
-
</select>
|
|
712
|
-
</label>
|
|
713
|
-
</div>
|
|
714
|
-
<p v-if="ttlHint" class="fx-perm-hint fx-perm-ttlhint" data-testid="share-ttl-hint">{{ ttlHint }}</p>
|
|
715
|
-
<button class="fx-perm-btn fx-perm-btn--primary fx-perm-create" :disabled="shareBusy" @click="createLink">
|
|
716
|
-
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
1008
|
+
<!-- Request files: the inbound drop link (folders only) -->
|
|
1009
|
+
<section v-if="isDir" class="fe-share__section">
|
|
1010
|
+
<button
|
|
1011
|
+
type="button"
|
|
1012
|
+
class="fe-share__sechead"
|
|
1013
|
+
:aria-expanded="open.drop ? 'true' : 'false'"
|
|
1014
|
+
data-testid="share-drop-toggle"
|
|
1015
|
+
@click="toggleSection('drop')"
|
|
1016
|
+
>
|
|
1017
|
+
<svg class="fe-share__caret" :class="{ 'is-open': open.drop }" viewBox="0 0 24 24" fill="none"
|
|
1018
|
+
stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
|
|
1019
|
+
aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
|
|
1020
|
+
<span class="fe-share__seclabel">{{ t('access.section.drop') }}</span>
|
|
1021
|
+
<span class="fe-share__secmeta">{{ dropSummary }}</span>
|
|
717
1022
|
</button>
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
<div class="
|
|
723
|
-
<
|
|
724
|
-
|
|
725
|
-
{{
|
|
726
|
-
</
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
<span>PIN: <code>{{ shareResult.pin }}</code></span>
|
|
734
|
-
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(shareResult.pin, 'sharepin')">
|
|
735
|
-
{{ copied === 'sharepin' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
736
|
-
</button>
|
|
1023
|
+
<div v-if="open.drop" class="fe-share__panel">
|
|
1024
|
+
<p class="fe-share__hint">
|
|
1025
|
+
{{ 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.') }}
|
|
1026
|
+
</p>
|
|
1027
|
+
<div class="fe-share__opts">
|
|
1028
|
+
<label class="fe-share__check">
|
|
1029
|
+
<input type="checkbox" v-model="dropPwd" />
|
|
1030
|
+
<span>{{ L('PIN ile koru', 'Protect with a PIN') }}</span>
|
|
1031
|
+
</label>
|
|
1032
|
+
<label class="fe-share__field">
|
|
1033
|
+
<span class="fe-share__fieldlabel">{{ L('Süre', 'Expiry') }}</span>
|
|
1034
|
+
<select v-model.number="dropExpiry" class="fe-share__select" data-testid="drop-expiry">
|
|
1035
|
+
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
1036
|
+
</select>
|
|
1037
|
+
</label>
|
|
737
1038
|
</div>
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
<span class="fx-perm-clilabel">{{ L('Komut satırı', 'Command line') }}</span>
|
|
742
|
-
<div class="fx-perm-clirow">
|
|
743
|
-
<code class="fx-perm-clicmd" :title="shareCli">{{ shareCli }}</code>
|
|
744
|
-
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(shareCli, 'sharecli')">
|
|
745
|
-
{{ copied === 'sharecli' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
746
|
-
</button>
|
|
747
|
-
</div>
|
|
748
|
-
</div>
|
|
749
|
-
|
|
750
|
-
<!-- send by email (one or more, comma/space separated) -->
|
|
751
|
-
<div class="fx-perm-mailrow">
|
|
752
|
-
<input v-model="shareMailTo" type="text" class="fx-perm-input" autocomplete="off"
|
|
753
|
-
:placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendShareMail" />
|
|
754
|
-
<button class="fx-perm-btn" :disabled="shareMailBusy" @click="sendShareMail">
|
|
755
|
-
{{ L('Gönder', 'Send') }}
|
|
756
|
-
</button>
|
|
757
|
-
</div>
|
|
758
|
-
<div v-if="shareMailNotice" class="fx-perm-notice">{{ shareMailNotice }}</div>
|
|
759
|
-
|
|
760
|
-
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
761
|
-
<button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(shareBody())">
|
|
762
|
-
<span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
|
|
1039
|
+
<p v-if="ttlHint" class="fe-share__hint">{{ ttlHint }}</p>
|
|
1040
|
+
<button type="button" class="fe-share__btn fe-share__btn--primary fe-share__btn--wide" data-testid="drop-create" :disabled="dropBusy" @click="createDropLink">
|
|
1041
|
+
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
763
1042
|
</button>
|
|
764
|
-
</div>
|
|
765
|
-
<p v-else-if="shareMailTo" class="fx-perm-hint">
|
|
766
|
-
{{ L('Aşağıdan bir bağlantı oluşturun, ardından', 'Create a link below, then it will be sent to') }}
|
|
767
|
-
<strong>{{ shareMailTo }}</strong> {{ L('adresine gönderin.', '.') }}
|
|
768
|
-
</p>
|
|
769
|
-
</div>
|
|
770
1043
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
<div v-if="!shares.length" class="fx-perm-empty">{{ L('Yok', 'None') }}</div>
|
|
774
|
-
<div v-for="s in shares" :key="s.uuid" class="fx-perm-person">
|
|
775
|
-
<span class="fx-perm-user fx-perm-link" :title="s.url">{{ s.url }}</span>
|
|
776
|
-
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(s.url, s.uuid)">
|
|
777
|
-
{{ copied === s.uuid ? L('✓', '✓') : L('Kopyala', 'Copy') }}
|
|
1044
|
+
<button type="button" class="fe-share__linkbtn" :aria-expanded="dropShowAdv ? 'true' : 'false'" @click="dropShowAdv = !dropShowAdv">
|
|
1045
|
+
{{ dropShowAdv ? L('Yükleme sınırlarını gizle', 'Hide upload limits') : L('Yükleme sınırları', 'Upload limits') }}
|
|
778
1046
|
</button>
|
|
779
|
-
<
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1047
|
+
<div v-if="dropShowAdv" class="fe-share__adv">
|
|
1048
|
+
<label class="fe-share__advrow">
|
|
1049
|
+
<span class="fe-share__fieldlabel">{{ L('En fazla dosya', 'Max files') }}</span>
|
|
1050
|
+
<input v-model="dropMaxFiles" type="number" min="1" class="fe-share__input fe-share__input--sm" placeholder="20" />
|
|
1051
|
+
</label>
|
|
1052
|
+
<label class="fe-share__advrow">
|
|
1053
|
+
<span class="fe-share__fieldlabel">{{ L('Dosya başı MB', 'MB / file') }}</span>
|
|
1054
|
+
<input v-model="dropMaxSizeMB" type="number" min="1" class="fe-share__input fe-share__input--sm" placeholder="500" />
|
|
1055
|
+
</label>
|
|
1056
|
+
<label class="fe-share__advrow">
|
|
1057
|
+
<span class="fe-share__fieldlabel">{{ L('İzinli türler', 'Allowed types') }}</span>
|
|
1058
|
+
<input v-model="dropAllowedExt" type="text" class="fe-share__input fe-share__input--sm" :placeholder="L('hepsi (örn. pdf, jpg)', 'all (e.g. pdf, jpg)')" />
|
|
1059
|
+
</label>
|
|
1060
|
+
<label class="fe-share__check">
|
|
1061
|
+
<input type="checkbox" v-model="dropAskName" />
|
|
1062
|
+
<span>{{ L('Yükleyenin adını sor', 'Ask uploader name') }}</span>
|
|
1063
|
+
</label>
|
|
1064
|
+
</div>
|
|
783
1065
|
|
|
784
|
-
|
|
785
|
-
<template v-if="tab === 'drop'">
|
|
786
|
-
<div class="fx-perm-addcard">
|
|
787
|
-
<p class="fx-perm-muted fx-perm-dropintro">
|
|
788
|
-
{{ 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.') }}
|
|
789
|
-
</p>
|
|
790
|
-
<div class="fx-perm-share-opts">
|
|
791
|
-
<label class="fx-perm-check">
|
|
792
|
-
<input type="checkbox" v-model="dropPwd" />
|
|
793
|
-
{{ L('PIN ile koru', 'Protect with a PIN') }}
|
|
794
|
-
</label>
|
|
795
|
-
<label class="fx-perm-field">
|
|
796
|
-
<span class="fx-perm-muted">{{ L('Süre', 'Expiry') }}</span>
|
|
797
|
-
<select v-model.number="dropExpiry" class="fx-perm-sel fx-perm-sel--sm" data-testid="drop-expiry">
|
|
798
|
-
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
799
|
-
</select>
|
|
800
|
-
</label>
|
|
801
|
-
</div>
|
|
802
|
-
<p v-if="ttlHint" class="fx-perm-hint fx-perm-ttlhint">{{ ttlHint }}</p>
|
|
803
|
-
<button class="fx-perm-btn fx-perm-btn--primary fx-perm-create" :disabled="dropBusy" @click="createDropLink">
|
|
804
|
-
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
805
|
-
</button>
|
|
1066
|
+
<div v-if="dropErr" class="fe-share__warn">{{ dropErr }}</div>
|
|
806
1067
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
</label>
|
|
827
|
-
</div>
|
|
1068
|
+
<template v-if="dropResult">
|
|
1069
|
+
<div class="fe-share__linkrow">
|
|
1070
|
+
<a :href="dropResult.url" target="_blank" rel="noopener" class="fe-share__url" :title="dropResult.url">{{ dropResult.url }}</a>
|
|
1071
|
+
<button type="button" class="fe-share__copy" data-testid="drop-copy" @click="copy(dropResult.url, 'drop')">
|
|
1072
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
1073
|
+
<span>{{ copied === 'drop' ? t('access.copied') : t('access.copy') }}</span>
|
|
1074
|
+
</button>
|
|
1075
|
+
</div>
|
|
1076
|
+
<p class="fe-share__detail">
|
|
1077
|
+
{{ validUntil(dropResult) }}
|
|
1078
|
+
<span v-if="dropResult.clamped">{{ L('(sunucu sınırı uygulandı)', '(server limit applied)') }}</span>
|
|
1079
|
+
</p>
|
|
1080
|
+
<div v-if="dropResult.pin" class="fe-share__pinrow">
|
|
1081
|
+
<span class="fe-share__pinlabel">PIN</span>
|
|
1082
|
+
<code class="fe-share__pin">{{ dropResult.pin }}</code>
|
|
1083
|
+
<button type="button" class="fe-share__mini" @click="copy(dropResult.pin, 'droppin')">
|
|
1084
|
+
{{ copied === 'droppin' ? t('access.copied') : t('access.copy') }}
|
|
1085
|
+
</button>
|
|
1086
|
+
</div>
|
|
828
1087
|
|
|
829
|
-
|
|
1088
|
+
<!-- email the upload link to one or more people -->
|
|
1089
|
+
<div class="fe-share__mailrow">
|
|
1090
|
+
<input v-model="dropMailTo" type="text" class="fe-share__input" autocomplete="off"
|
|
1091
|
+
:placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendDropMail" />
|
|
1092
|
+
<button type="button" class="fe-share__btn" :disabled="dropMailBusy" @click="sendDropMail">
|
|
1093
|
+
{{ L('Gönder', 'Send') }}
|
|
1094
|
+
</button>
|
|
1095
|
+
</div>
|
|
1096
|
+
<div v-if="dropMailNotice" class="fe-share__notice">{{ dropMailNotice }}</div>
|
|
830
1097
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
{{ copied === 'drop' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
1098
|
+
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
1099
|
+
<button v-if="canShare" type="button" class="fe-share__btn fe-share__btn--wide" @click="nativeShare(dropBody())">
|
|
1100
|
+
<span aria-hidden="true" v-html="actionIconSvg('access')"></span>
|
|
1101
|
+
<span>{{ L('Paylaş', 'Share') }}</span>
|
|
836
1102
|
</button>
|
|
837
|
-
</
|
|
838
|
-
<div class="fx-perm-muted fx-perm-validuntil">
|
|
839
|
-
{{ validUntil(dropResult) }}
|
|
840
|
-
<span v-if="dropResult.clamped">{{ L('(sunucu sınırı uygulandı)', '(server limit applied)') }}</span>
|
|
841
|
-
</div>
|
|
842
|
-
<div v-if="dropResult.pin" class="fx-perm-pin">
|
|
843
|
-
<span>PIN: <code>{{ dropResult.pin }}</code></span>
|
|
844
|
-
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(dropResult.pin, 'droppin')">
|
|
845
|
-
{{ copied === 'droppin' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
846
|
-
</button>
|
|
847
|
-
</div>
|
|
1103
|
+
</template>
|
|
848
1104
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
<
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1105
|
+
<div class="fe-share__list">
|
|
1106
|
+
<h4 class="fe-share__listhead">{{ L('Mevcut bağlantılar', 'Existing links') }}</h4>
|
|
1107
|
+
<p v-if="!dropShares.length" class="fe-share__empty">{{ L('Yok', 'None') }}</p>
|
|
1108
|
+
<div v-for="s in dropShares" :key="s.uuid" class="fe-share__row">
|
|
1109
|
+
<span class="fe-share__url" :title="s.url">{{ s.url }}</span>
|
|
1110
|
+
<button type="button" class="fe-share__mini" @click="copy(s.url, s.uuid)">
|
|
1111
|
+
{{ copied === s.uuid ? t('access.copied') : t('access.copy') }}
|
|
1112
|
+
</button>
|
|
1113
|
+
<button type="button" class="fe-share__del" :disabled="shareBusy" :title="L('İptal', 'Revoke')"
|
|
1114
|
+
:aria-label="L('İptal', 'Revoke')" @click="revoke(s)">
|
|
1115
|
+
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
1116
|
+
</button>
|
|
1117
|
+
</div>
|
|
856
1118
|
</div>
|
|
857
|
-
<div v-if="dropMailNotice" class="fx-perm-notice">{{ dropMailNotice }}</div>
|
|
858
|
-
|
|
859
|
-
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
860
|
-
<button v-if="canShare" type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-sharebtn" @click="nativeShare(dropBody())">
|
|
861
|
-
<span aria-hidden="true">📤</span> {{ L('Paylaş', 'Share') }}
|
|
862
|
-
</button>
|
|
863
1119
|
</div>
|
|
864
|
-
</
|
|
865
|
-
</
|
|
1120
|
+
</section>
|
|
1121
|
+
</div>
|
|
866
1122
|
</div>
|
|
1123
|
+
|
|
1124
|
+
<footer class="fe-share__foot">
|
|
1125
|
+
<button type="button" class="fe-share__btn fe-share__btn--primary" data-testid="share-done" @click="emit('close')">
|
|
1126
|
+
{{ t('access.done') }}
|
|
1127
|
+
</button>
|
|
1128
|
+
</footer>
|
|
867
1129
|
</div>
|
|
868
1130
|
</div>
|
|
869
1131
|
</template>
|
|
870
|
-
|
|
871
|
-
<style>
|
|
872
|
-
/* ⚠ NOT `scoped`, deliberately. Vue's scoped styles compile to
|
|
873
|
-
`.cls[data-v-HASH]`, and in the web-component build the hash baked into
|
|
874
|
-
this CSS does not match the one Vue stamps onto the DOM — so every rule
|
|
875
|
-
here silently stopped applying. Measured in the desktop app: the share
|
|
876
|
-
dialog had `position: static`, no background and no radius, i.e. raw
|
|
877
|
-
unstyled HTML, in EVERY embedded surface.
|
|
878
|
-
Safe to drop: every selector below is prefixed (fx-/fe-/filex-), so
|
|
879
|
-
there is nothing here that can leak into a host page. */
|
|
880
|
-
.fx-perm-overlay {
|
|
881
|
-
position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5);
|
|
882
|
-
display: flex; align-items: center; justify-content: center; z-index: 10000;
|
|
883
|
-
font-family: var(--fe-font);
|
|
884
|
-
}
|
|
885
|
-
.fx-perm-modal {
|
|
886
|
-
display: flex; flex-direction: column;
|
|
887
|
-
background: var(--fe-bg); color: var(--fe-text);
|
|
888
|
-
width: min(520px, 94vw); max-height: 86vh;
|
|
889
|
-
border: 1px solid var(--fe-border); border-radius: var(--fe-radius-lg, 14px);
|
|
890
|
-
box-shadow: var(--fe-shadow); font-size: 14px; overflow: hidden;
|
|
891
|
-
}
|
|
892
|
-
/* header + tabs are fixed; only the body scrolls */
|
|
893
|
-
.fx-perm-head {
|
|
894
|
-
display: flex; align-items: center; justify-content: space-between; gap: 10px;
|
|
895
|
-
padding: 16px 18px 12px; flex: none;
|
|
896
|
-
}
|
|
897
|
-
.fx-perm-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
898
|
-
.fx-perm-ico { font-size: 22px; line-height: 1; }
|
|
899
|
-
.fx-perm-titletext { display: flex; flex-direction: column; min-width: 0; }
|
|
900
|
-
.fx-perm-title h3 { margin: 0; font-size: 15px; font-weight: 600; color: var(--fe-text); }
|
|
901
|
-
.fx-perm-sub {
|
|
902
|
-
font-size: 12px; color: var(--fe-text); max-width: 340px;
|
|
903
|
-
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
904
|
-
}
|
|
905
|
-
.fx-perm-subdim { color: var(--fe-text-muted); }
|
|
906
|
-
.fx-perm-x {
|
|
907
|
-
background: none; border: none; font-size: 17px; cursor: pointer;
|
|
908
|
-
color: var(--fe-text-muted); flex: none; line-height: 1; padding: 2px 4px; border-radius: 6px;
|
|
909
|
-
}
|
|
910
|
-
.fx-perm-x:hover { color: var(--fe-text); background: var(--fe-bg-hover); }
|
|
911
|
-
.fx-perm-tabs { display: flex; gap: 2px; padding: 0 18px; border-bottom: 1px solid var(--fe-border); flex: none; }
|
|
912
|
-
.fx-perm-tab {
|
|
913
|
-
background: none; border: none; border-bottom: 2px solid transparent;
|
|
914
|
-
padding: 8px 12px; cursor: pointer; color: var(--fe-text-muted); font-size: 13px;
|
|
915
|
-
font-family: inherit; font-weight: 500;
|
|
916
|
-
}
|
|
917
|
-
.fx-perm-tab:hover { color: var(--fe-text); }
|
|
918
|
-
.fx-perm-tab.is-active { color: var(--fe-primary); border-bottom-color: var(--fe-primary); }
|
|
919
|
-
.fx-perm-body { flex: 1 1 auto; overflow-y: auto; padding: 14px 18px 18px; }
|
|
920
|
-
|
|
921
|
-
.fx-perm-addcard {
|
|
922
|
-
padding: 12px; border-radius: var(--fe-radius-md);
|
|
923
|
-
background: var(--fe-bg-elev); border: 1px solid var(--fe-border);
|
|
924
|
-
}
|
|
925
|
-
.fx-perm-section { margin-top: 16px; }
|
|
926
|
-
.fx-perm-section h4 {
|
|
927
|
-
margin: 0 0 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fe-text-muted);
|
|
928
|
-
}
|
|
929
|
-
.fx-perm-empty { color: var(--fe-text-muted); font-size: 13px; padding: 8px 2px; }
|
|
930
|
-
|
|
931
|
-
/* person rows */
|
|
932
|
-
.fx-perm-person { display: flex; align-items: center; gap: 10px; padding: 7px 2px; }
|
|
933
|
-
.fx-perm-person + .fx-perm-person { border-top: 1px solid var(--fe-border); }
|
|
934
|
-
.fx-perm-inh { color: var(--fe-text-muted); }
|
|
935
|
-
.fx-perm-av {
|
|
936
|
-
width: 28px; height: 28px; flex: none; border-radius: 50%;
|
|
937
|
-
display: flex; align-items: center; justify-content: center;
|
|
938
|
-
background: var(--fe-primary); color: var(--fe-text-on-primary, #fff);
|
|
939
|
-
font-size: 12px; font-weight: 600;
|
|
940
|
-
}
|
|
941
|
-
.fx-perm-av--dim { background: var(--fe-bg-hover); color: var(--fe-text-muted); }
|
|
942
|
-
.fx-perm-user { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fe-text); min-width: 0; }
|
|
943
|
-
.fx-perm-from { font-size: 11px; color: var(--fe-text-muted); white-space: nowrap; }
|
|
944
|
-
.fx-perm-badge {
|
|
945
|
-
font-size: 11px; padding: 2px 9px; border-radius: 999px;
|
|
946
|
-
background: var(--fe-bg-hover); color: var(--fe-text);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
/* inputs / selects / buttons */
|
|
950
|
-
.fx-perm-sel, .fx-perm-input {
|
|
951
|
-
padding: 7px 9px; border-radius: var(--fe-radius-sm, 7px); border: 1px solid var(--fe-border);
|
|
952
|
-
background: var(--fe-bg); color: var(--fe-text); font-size: 13px; font-family: inherit;
|
|
953
|
-
}
|
|
954
|
-
.fx-perm-sel--sm { padding: 5px 7px; font-size: 12px; }
|
|
955
|
-
.fx-perm-sel:focus, .fx-perm-input:focus { outline: none; border-color: var(--fe-primary); }
|
|
956
|
-
.fx-perm-add { display: flex; gap: 8px; align-items: stretch; }
|
|
957
|
-
.fx-perm-emailwrap { position: relative; flex: 1; min-width: 120px; }
|
|
958
|
-
.fx-perm-input { width: 100%; box-sizing: border-box; }
|
|
959
|
-
.fx-perm-suggest {
|
|
960
|
-
position: absolute; top: calc(100% + 3px); left: 0; right: 0; z-index: 5; margin: 0; padding: 4px;
|
|
961
|
-
list-style: none; background: var(--fe-bg); border: 1px solid var(--fe-border);
|
|
962
|
-
border-radius: var(--fe-radius-sm, 7px); box-shadow: var(--fe-shadow); max-height: 210px; overflow: auto;
|
|
963
|
-
}
|
|
964
|
-
.fx-perm-suggest li { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; cursor: pointer; }
|
|
965
|
-
.fx-perm-suggest li:hover { background: var(--fe-bg-hover); }
|
|
966
|
-
.fx-perm-suggest-av {
|
|
967
|
-
width: 24px; height: 24px; flex: none; border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
|
968
|
-
background: var(--fe-bg-hover); color: var(--fe-text); font-size: 11px; font-weight: 600;
|
|
969
|
-
}
|
|
970
|
-
.fx-perm-suggest-txt { display: flex; flex-direction: column; min-width: 0; }
|
|
971
|
-
.fx-perm-suggest-name { color: var(--fe-text); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
972
|
-
.fx-perm-suggest-meta { color: var(--fe-text-muted); font-size: 11px; }
|
|
973
|
-
.fx-perm-check { display: inline-flex; gap: 6px; align-items: center; font-size: 13px; color: var(--fe-text); cursor: pointer; }
|
|
974
|
-
.fx-perm-btn {
|
|
975
|
-
padding: 7px 13px; border-radius: var(--fe-radius-sm, 7px); border: 1px solid var(--fe-border);
|
|
976
|
-
background: var(--fe-bg); color: var(--fe-text); font-size: 13px; font-family: inherit; cursor: pointer; white-space: nowrap;
|
|
977
|
-
}
|
|
978
|
-
.fx-perm-btn:hover:not(:disabled) { background: var(--fe-bg-hover); }
|
|
979
|
-
.fx-perm-btn:disabled { opacity: 0.55; cursor: default; }
|
|
980
|
-
.fx-perm-btn--sm { padding: 5px 10px; font-size: 12px; }
|
|
981
|
-
.fx-perm-btn--primary { background: var(--fe-primary); border-color: var(--fe-primary); color: var(--fe-text-on-primary, #fff); }
|
|
982
|
-
.fx-perm-btn--primary:hover:not(:disabled) { background: var(--fe-primary-hover, var(--fe-primary)); filter: brightness(1.05); }
|
|
983
|
-
.fx-perm-btn--ghost { background: none; border-color: transparent; color: var(--fe-primary); padding-left: 2px; }
|
|
984
|
-
.fx-perm-btn--ghost:hover:not(:disabled) { background: none; text-decoration: underline; }
|
|
985
|
-
.fx-perm-del { background: none; border: none; color: var(--fe-danger); cursor: pointer; font-size: 14px; flex: none; padding: 2px 4px; }
|
|
986
|
-
.fx-perm-del:hover:not(:disabled) { filter: brightness(1.15); }
|
|
987
|
-
|
|
988
|
-
/* invite sub-flow */
|
|
989
|
-
.fx-perm-invite {
|
|
990
|
-
margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--fe-border);
|
|
991
|
-
}
|
|
992
|
-
.fx-perm-invite-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 6px; }
|
|
993
|
-
.fx-perm-invite-row { display: flex; gap: 8px; align-items: stretch; }
|
|
994
|
-
.fx-perm-invite-row .fx-perm-btn--primary { flex: 1; }
|
|
995
|
-
|
|
996
|
-
/* share options + result
|
|
997
|
-
⚠ These options used to be one wrapping flex row with the primary button
|
|
998
|
-
pushed to its right end by `margin-left: auto`. That works with two controls
|
|
999
|
-
and breaks with three: at the modal's 520px the row wraps and the button
|
|
1000
|
-
drops onto a line of its own, right-aligned and looking dislodged — which is
|
|
1001
|
-
exactly what adding the download limit did. A two-column grid of fields with
|
|
1002
|
-
the action underneath holds its shape whatever we add next. */
|
|
1003
|
-
.fx-perm-share-opts { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 12px; align-items: end; }
|
|
1004
|
-
.fx-perm-share-opts > .fx-perm-check { grid-column: 1 / -1; }
|
|
1005
|
-
.fx-perm-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; min-width: 0; }
|
|
1006
|
-
.fx-perm-field .fx-perm-sel { width: 100%; }
|
|
1007
|
-
.fx-perm-create { width: 100%; margin-top: 10px; }
|
|
1008
|
-
@media (max-width: 380px) { .fx-perm-share-opts { grid-template-columns: 1fr; } }
|
|
1009
|
-
.fx-perm-result { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--fe-border); }
|
|
1010
|
-
.fx-perm-linkrow { display: flex; gap: 8px; align-items: center; }
|
|
1011
|
-
.fx-perm-link { color: var(--fe-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; text-decoration: none; }
|
|
1012
|
-
.fx-perm-link:hover { text-decoration: underline; }
|
|
1013
|
-
.fx-perm-pin { margin: 8px 0 0; font-size: 13px; color: var(--fe-text); display: flex; align-items: center; gap: 8px; }
|
|
1014
|
-
.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; }
|
|
1015
|
-
/* one-line curl for the fresh link */
|
|
1016
|
-
.fx-perm-cli { margin-top: 10px; }
|
|
1017
|
-
.fx-perm-clilabel { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fe-text-muted); margin-bottom: 4px; }
|
|
1018
|
-
.fx-perm-clirow { display: flex; gap: 8px; align-items: center; }
|
|
1019
|
-
.fx-perm-clicmd {
|
|
1020
|
-
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
1021
|
-
font-family: var(--fe-font-mono, monospace); font-size: 12px;
|
|
1022
|
-
background: var(--fe-bg-hover); color: var(--fe-text);
|
|
1023
|
-
padding: 6px 8px; border-radius: var(--fe-radius-sm, 7px);
|
|
1024
|
-
}
|
|
1025
|
-
.fx-perm-mailrow { display: flex; gap: 8px; margin-top: 10px; }
|
|
1026
|
-
.fx-perm-mailrow .fx-perm-input { flex: 1; }
|
|
1027
|
-
.fx-perm-hint { font-size: 13px; color: var(--fe-text-muted); margin: 10px 0 0; }
|
|
1028
|
-
.fx-perm-ttlhint { margin: 4px 0 8px; }
|
|
1029
|
-
.fx-perm-validuntil { margin: 4px 0 6px; }
|
|
1030
|
-
/* native "Share" button — sits under the mail row, full width */
|
|
1031
|
-
.fx-perm-sharebtn { display: flex; width: 100%; align-items: center; justify-content: center; gap: 6px; margin-top: 8px; }
|
|
1032
|
-
|
|
1033
|
-
/* file-drop tab */
|
|
1034
|
-
.fx-perm-dropintro { margin: 0 0 12px; line-height: 1.4; }
|
|
1035
|
-
.fx-perm-advtoggle { margin-top: 10px; padding-left: 2px; }
|
|
1036
|
-
.fx-perm-adv {
|
|
1037
|
-
margin-top: 8px; padding-top: 10px; border-top: 1px dashed var(--fe-border);
|
|
1038
|
-
display: flex; flex-direction: column; gap: 8px;
|
|
1039
|
-
}
|
|
1040
|
-
.fx-perm-adv-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; }
|
|
1041
|
-
.fx-perm-input--sm { width: 130px; padding: 5px 8px; font-size: 12px; }
|
|
1042
|
-
|
|
1043
|
-
.fx-perm-warn {
|
|
1044
|
-
background: rgba(245, 158, 11, 0.14); border: 1px solid rgba(245, 158, 11, 0.4);
|
|
1045
|
-
border-radius: var(--fe-radius-sm, 7px); padding: 8px 10px; font-size: 13px; margin-bottom: 10px; color: var(--fe-text);
|
|
1046
|
-
}
|
|
1047
|
-
.fx-perm-notice { margin-top: 8px; font-size: 13px; color: var(--fe-text-muted); }
|
|
1048
|
-
.fx-perm-reveal { margin-top: 8px; font-size: 13px; word-break: break-all; color: var(--fe-text); }
|
|
1049
|
-
.fx-perm-muted { color: var(--fe-text-muted); font-size: 13px; }
|
|
1050
|
-
</style>
|