@brftech/filex-core 0.39.1 → 0.41.0
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-BNqukFg8.js +124 -0
- package/dist/ArchiveViewer-BNqukFg8.js.map +1 -0
- package/dist/CsvViewer-ii_-MgmG.js +141 -0
- package/dist/CsvViewer-ii_-MgmG.js.map +1 -0
- package/dist/{DrawioViewer-BNALOB04.js → DrawioViewer-B2tuu4rX.js} +48 -42
- package/dist/DrawioViewer-B2tuu4rX.js.map +1 -0
- package/dist/EpubViewer-p4B8iiWb.js +163 -0
- package/dist/EpubViewer-p4B8iiWb.js.map +1 -0
- package/dist/IpynbViewer-D_qYRJhu.js +184 -0
- package/dist/IpynbViewer-D_qYRJhu.js.map +1 -0
- package/dist/MermaidViewer-3ZkfNM8v.js +137 -0
- package/dist/MermaidViewer-3ZkfNM8v.js.map +1 -0
- package/dist/PsdViewer-CuV-OPSd.js +122 -0
- package/dist/PsdViewer-CuV-OPSd.js.map +1 -0
- package/dist/TiffViewer-BLeoF4Be.js +142 -0
- package/dist/TiffViewer-BLeoF4Be.js.map +1 -0
- package/dist/Viewer3D-DHhhU76E.js +75 -0
- package/dist/Viewer3D-DHhhU76E.js.map +1 -0
- package/dist/filex-core.js +217 -20077
- 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-BraG7Cz4.js +26664 -0
- package/dist/index-BraG7Cz4.js.map +1 -0
- package/dist/index.d.ts +4276 -851
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +2170 -492
- 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 +57 -2
- package/src/components/EncryptedFolderModal.vue +31 -25
- package/src/components/FilePane.vue +939 -0
- package/src/components/FilterBar.vue +568 -72
- package/src/components/GalleryView.vue +162 -30
- package/src/components/GridView.vue +322 -58
- package/src/components/HomeView.vue +331 -0
- package/src/components/InspectorPanel.vue +296 -53
- package/src/components/ListView.vue +1158 -167
- package/src/components/NFSExportsPanel.vue +7 -4
- package/src/components/OnboardingTour.vue +156 -33
- package/src/components/QuickLook.vue +34 -5
- 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 +43 -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 +127 -0
- package/src/components/TimeZonePicker.vue +696 -0
- package/src/components/TokensPanel.vue +16 -4
- package/src/components/Toolbar.vue +965 -469
- package/src/components/ViewSwitcher.vue +9 -4
- package/src/composables/useConnections.ts +35 -4
- package/src/composables/useExplorerTimeZone.ts +85 -0
- package/src/composables/useFileApi.ts +53 -4
- package/src/composables/useKeyboardShortcuts.ts +122 -0
- package/src/composables/useLocale.ts +365 -25
- package/src/index.ts +182 -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 +11 -1
- package/src/lib/sortOrder.ts +459 -0
- package/src/lib/themes.ts +152 -23
- package/src/lib/timezone.ts +456 -0
- package/src/lib/uiProfile.ts +82 -0
- package/src/lib/viewPrefs.ts +1036 -0
- package/src/locales/en.ts +432 -25
- package/src/locales/tr.ts +425 -25
- 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 +543 -473
- package/src/modals/PreviewModal.vue +516 -156
- package/src/styles/base.css +5435 -8
- package/src/styles/variables.css +140 -60
- 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,41 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
*/
|
|
7
30
|
import { ref, onMounted, onBeforeUnmount, computed, 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
34
|
import { STOCK_EXPIRY_DAYS, clampExpiryOptions, defaultExpiryDays, ttlCeilingHint, validUntilLine } from '../lib/shareTtl';
|
|
12
35
|
import { resolveLocale } from '../locales/resolve';
|
|
36
|
+
import { formatByteSize, useLocale } from '../composables/useLocale';
|
|
37
|
+
import { actionIconSvg } from '../lib/actionIcons';
|
|
38
|
+
import { fileIconTile } from '../lib/fileIcons';
|
|
13
39
|
|
|
14
40
|
const props = defineProps<{
|
|
15
41
|
api: FileApi;
|
|
@@ -21,23 +47,24 @@ const props = defineProps<{
|
|
|
21
47
|
* undefined/0 = no ceiling. The expiry choices are derived from it. */
|
|
22
48
|
shareMaxTtlDays?: number;
|
|
23
49
|
/**
|
|
24
|
-
* surucu:d1 — which
|
|
25
|
-
* every existing caller has.
|
|
50
|
+
* surucu:d1 — which part of the dialog the caller wants opened. Absent =
|
|
51
|
+
* 'perms', the behaviour every existing caller has.
|
|
26
52
|
*
|
|
27
|
-
* The drive shell's "Request files" needs 'drop': the file-drop link
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* what it does.
|
|
53
|
+
* The drive shell's "Request files" needs 'drop': the file-drop link lives
|
|
54
|
+
* in this dialog, and a menu entry that opened it anywhere else would be a
|
|
55
|
+
* menu entry that lied about what it does.
|
|
31
56
|
*
|
|
32
|
-
* ⚠ A hint, not a lock: `load()` still falls back to
|
|
33
|
-
* turns out not to be an owner, because the
|
|
34
|
-
* see.
|
|
57
|
+
* ⚠ A hint, not a lock: `load()` still falls back to the link tier when the
|
|
58
|
+
* caller turns out not to be an owner, because the grants section is not
|
|
59
|
+
* theirs to see.
|
|
35
60
|
*/
|
|
36
61
|
initialTab?: 'perms' | 'share' | 'drop';
|
|
37
62
|
}>();
|
|
38
63
|
const emit = defineEmits<{ (e: 'close'): void }>();
|
|
39
64
|
|
|
40
|
-
const
|
|
65
|
+
const localeCode = computed(() => resolveLocale(props.locale));
|
|
66
|
+
const { t } = useLocale(() => localeCode.value);
|
|
67
|
+
const tr = computed(() => localeCode.value !== 'en');
|
|
41
68
|
function L(t: string, e: string): string {
|
|
42
69
|
return tr.value ? t : e;
|
|
43
70
|
}
|
|
@@ -50,10 +77,23 @@ const pathParts = computed(() => {
|
|
|
50
77
|
const segs = rel.split('/').filter(Boolean);
|
|
51
78
|
return { adapter, name: segs.length ? segs[segs.length - 1] : adapter, rel };
|
|
52
79
|
});
|
|
80
|
+
// The type tile beside the title — the same badge the listing draws, so the
|
|
81
|
+
// dialog is visibly about the row the user right-clicked.
|
|
82
|
+
const titleTile = computed(() => {
|
|
83
|
+
const name = pathParts.value.name;
|
|
84
|
+
const dot = name.lastIndexOf('.');
|
|
85
|
+
return fileIconTile({
|
|
86
|
+
type: props.isDir ? 'dir' : 'file',
|
|
87
|
+
extension: !props.isDir && dot > 0 ? name.slice(dot + 1) : '',
|
|
88
|
+
});
|
|
89
|
+
});
|
|
53
90
|
|
|
54
|
-
type
|
|
55
|
-
const
|
|
56
|
-
|
|
91
|
+
type Section = 'link' | 'people' | 'drop';
|
|
92
|
+
const open = ref<Record<Section, boolean>>({ link: false, people: false, drop: false });
|
|
93
|
+
function toggleSection(s: Section) {
|
|
94
|
+
open.value = { ...open.value, [s]: !open.value[s] };
|
|
95
|
+
}
|
|
96
|
+
const canManage = ref(false); // owner/admin → can see the grants section
|
|
57
97
|
|
|
58
98
|
// ── permissions state ──
|
|
59
99
|
const loading = ref(true);
|
|
@@ -94,6 +134,23 @@ const shareMailTo = ref('');
|
|
|
94
134
|
const shareMailBusy = ref(false);
|
|
95
135
|
const shareMailNotice = ref('');
|
|
96
136
|
|
|
137
|
+
/**
|
|
138
|
+
* ⚠ `listShares` returns BOTH kinds of link — a download link (`/s/…`) and a
|
|
139
|
+
* file-drop link (`/d/…`) are the same row with a different `kind`. They used
|
|
140
|
+
* to be listed together under "Existing links", which meant an upload link
|
|
141
|
+
* appeared in the download tab and the "link sharing" state could not be read
|
|
142
|
+
* off the list at all. Split at the source: each section owns its own kind.
|
|
143
|
+
*
|
|
144
|
+
* `kind` is absent from the `ShareInfo` type but present on the wire (see
|
|
145
|
+
* backend/internal/api/handlers/share.go), so it is read through a narrow cast
|
|
146
|
+
* rather than by widening a shared type this dialog does not own.
|
|
147
|
+
*/
|
|
148
|
+
function shareKind(s: ShareInfo): string {
|
|
149
|
+
return (s as ShareInfo & { kind?: string }).kind ?? 'download';
|
|
150
|
+
}
|
|
151
|
+
const downloadShares = computed(() => shares.value.filter((s) => shareKind(s) !== 'drop'));
|
|
152
|
+
const dropShares = computed(() => shares.value.filter((s) => shareKind(s) === 'drop'));
|
|
153
|
+
|
|
97
154
|
// ⚠ Derived from the server's ceiling, never a fixed list: offering "30 days"
|
|
98
155
|
// on a server that keeps links for 7 would show a choice that is not one.
|
|
99
156
|
// The same helper drives every surface (see lib/shareTtl.ts).
|
|
@@ -195,14 +252,15 @@ async function reload() {
|
|
|
195
252
|
storageRbac.value = r.storage_rbac;
|
|
196
253
|
canManage.value = true;
|
|
197
254
|
} catch (e) {
|
|
198
|
-
// 403 = caller is editor (not owner): no
|
|
255
|
+
// 403 = caller is editor (not owner): no grants section, link only.
|
|
199
256
|
const st = (e as { status?: number }).status;
|
|
200
257
|
if (st === 403) {
|
|
201
258
|
canManage.value = false;
|
|
202
|
-
// Editor (not owner):
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
|
|
259
|
+
// Editor (not owner): the grants section is not theirs — close it and
|
|
260
|
+
// leave the link tier, which is the whole dialog for them.
|
|
261
|
+
// ⚠ Unless the caller asked for the drop link, which an editor may
|
|
262
|
+
// perfectly well mint.
|
|
263
|
+
open.value = { ...open.value, people: false };
|
|
206
264
|
} else {
|
|
207
265
|
err.value = e instanceof Error ? e.message : String(e);
|
|
208
266
|
}
|
|
@@ -218,12 +276,29 @@ async function reloadShares() {
|
|
|
218
276
|
shares.value = [];
|
|
219
277
|
}
|
|
220
278
|
}
|
|
279
|
+
function onKey(e: KeyboardEvent) {
|
|
280
|
+
if (e.key === 'Escape') emit('close');
|
|
281
|
+
}
|
|
221
282
|
onMounted(async () => {
|
|
283
|
+
// surucu:d1 — the caller's entry point decides which section opens with the
|
|
284
|
+
// dialog.
|
|
285
|
+
//
|
|
286
|
+
// ⚠ The DEFAULT (no `initialTab`, i.e. the plain "Share / Permissions"
|
|
287
|
+
// action) now opens NOTHING. It used to land on the permissions tab, which
|
|
288
|
+
// meant the common case — turn the link on, copy it — arrived buried under a
|
|
289
|
+
// grant editor. The link tier is the answer to the default question; the
|
|
290
|
+
// grants are one click away with their own state ("2 people") readable from
|
|
291
|
+
// the closed heading. A caller that explicitly wants the grants still says
|
|
292
|
+
// so, and "Request files" still lands on the drop link.
|
|
293
|
+
if (props.initialTab === 'drop') open.value = { ...open.value, drop: true };
|
|
294
|
+
else if (props.initialTab === 'perms') open.value = { ...open.value, people: true };
|
|
295
|
+
document.addEventListener('keydown', onKey);
|
|
222
296
|
await reload();
|
|
223
297
|
await reloadShares();
|
|
224
298
|
});
|
|
225
299
|
onBeforeUnmount(() => {
|
|
226
300
|
if (searchTimer) clearTimeout(searchTimer);
|
|
301
|
+
document.removeEventListener('keydown', onKey);
|
|
227
302
|
});
|
|
228
303
|
|
|
229
304
|
function onEmailInput() {
|
|
@@ -315,13 +390,14 @@ async function inviteCreateUser() {
|
|
|
315
390
|
busy.value = false;
|
|
316
391
|
}
|
|
317
392
|
}
|
|
318
|
-
// "Just send a share link" →
|
|
319
|
-
// owner creates a link (with their chosen expiry/PIN) and mails it
|
|
393
|
+
// "Just send a share link" → open the link options with the address prefilled
|
|
394
|
+
// so the owner creates a link (with their chosen expiry/PIN) and mails it
|
|
395
|
+
// from there.
|
|
320
396
|
function gotoShareWithMail() {
|
|
321
397
|
shareMailTo.value = email.value.trim().toLowerCase();
|
|
322
398
|
noAccount.value = false;
|
|
323
399
|
notice.value = '';
|
|
324
|
-
|
|
400
|
+
open.value = { ...open.value, link: true };
|
|
325
401
|
}
|
|
326
402
|
async function changeLevel(g: Grant, newLevel: string) {
|
|
327
403
|
if (newLevel === g.level) return;
|
|
@@ -375,6 +451,39 @@ async function createLink() {
|
|
|
375
451
|
shareBusy.value = false;
|
|
376
452
|
}
|
|
377
453
|
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* gorunum:v2-share — the switch.
|
|
457
|
+
*
|
|
458
|
+
* ON with nothing minted mints one with whatever the options tier currently
|
|
459
|
+
* says (that is the default: the server's ceiling as the expiry, no PIN, no
|
|
460
|
+
* cap). OFF revokes every DOWNLOAD link on this item, because that is what
|
|
461
|
+
* "link sharing is off" has to mean — a switch that left a live link behind
|
|
462
|
+
* would be the most dangerous control in the dialog.
|
|
463
|
+
*
|
|
464
|
+
* ⚠ Drop links are deliberately untouched: they are an inbound door managed by
|
|
465
|
+
* its own section, and revoking someone's upload invitation because the owner
|
|
466
|
+
* turned off downloads is not what either control says.
|
|
467
|
+
*/
|
|
468
|
+
async function toggleLink() {
|
|
469
|
+
if (shareBusy.value) return;
|
|
470
|
+
if (linkOn.value) {
|
|
471
|
+
shareBusy.value = true;
|
|
472
|
+
shareErr.value = '';
|
|
473
|
+
try {
|
|
474
|
+
for (const s of downloadShares.value) await props.api.revokeShare(s.uuid);
|
|
475
|
+
shareResult.value = null;
|
|
476
|
+
await reloadShares();
|
|
477
|
+
} catch (e) {
|
|
478
|
+
shareErr.value = e instanceof Error ? e.message : String(e);
|
|
479
|
+
} finally {
|
|
480
|
+
shareBusy.value = false;
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
await createLink();
|
|
485
|
+
}
|
|
486
|
+
|
|
378
487
|
async function sendShareMail() {
|
|
379
488
|
const list = splitEmails(shareMailTo.value);
|
|
380
489
|
if (!list.length) {
|
|
@@ -437,6 +546,7 @@ async function createDropLink() {
|
|
|
437
546
|
expiresAt: r.share.expires_at ?? null,
|
|
438
547
|
clamped: !!r.share.expiry_clamped,
|
|
439
548
|
};
|
|
549
|
+
await reloadShares();
|
|
440
550
|
} catch (e) {
|
|
441
551
|
dropErr.value = e instanceof Error ? e.message : String(e);
|
|
442
552
|
} finally {
|
|
@@ -475,8 +585,15 @@ async function sendDropMail() {
|
|
|
475
585
|
}
|
|
476
586
|
async function revoke(s: ShareInfo) {
|
|
477
587
|
shareBusy.value = true;
|
|
478
|
-
try {
|
|
479
|
-
|
|
588
|
+
try {
|
|
589
|
+
await props.api.revokeShare(s.uuid);
|
|
590
|
+
// ⚠ The lead tier reads `shareResult` first. Revoking the link that is
|
|
591
|
+
// showing up there has to clear it too, or the dialog goes on offering a
|
|
592
|
+
// Copy button — and a switch reading ON — for a link the server has just
|
|
593
|
+
// thrown away.
|
|
594
|
+
if (shareResult.value && shareResult.value.url === s.url) shareResult.value = null;
|
|
595
|
+
await reloadShares();
|
|
596
|
+
} catch (e) { shareErr.value = e instanceof Error ? e.message : String(e); }
|
|
480
597
|
finally { shareBusy.value = false; }
|
|
481
598
|
}
|
|
482
599
|
function copy(text: string, tag = 'url') {
|
|
@@ -493,15 +610,24 @@ function copy(text: string, tag = 'url') {
|
|
|
493
610
|
// same as an emailed link.
|
|
494
611
|
const canShare = computed(() => typeof navigator !== 'undefined' && typeof navigator.share === 'function');
|
|
495
612
|
|
|
496
|
-
|
|
613
|
+
/**
|
|
614
|
+
* The size line in the share message, spelled the way the SERVER spells it.
|
|
615
|
+
*
|
|
616
|
+
* ⚠ This one really does have to mirror `humanSize()` in
|
|
617
|
+
* backend/internal/api/handlers/mail_templates.go — the same file forwarded
|
|
618
|
+
* by e-mail and by the OS share sheet must not carry two different sizes. So
|
|
619
|
+
* the mirroring is stated as arguments to the one formatter (1024, always one
|
|
620
|
+
* decimal, a dot and English letters, because that is what Go's `%.1f %cB`
|
|
621
|
+
* prints) instead of being a fifth private copy of the arithmetic.
|
|
622
|
+
*
|
|
623
|
+
* ⚠ It is therefore the one place in the UI that is NOT decimal, so a share
|
|
624
|
+
* message reads "1.4 MB" where the listing row reads "1.5 MB". The register
|
|
625
|
+
* entry for this was right about the real fix: the e-mail should render from
|
|
626
|
+
* one source rather than the UI copying its rounding.
|
|
627
|
+
*/
|
|
497
628
|
function humanSize(b?: number): string {
|
|
498
629
|
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`;
|
|
630
|
+
return formatByteSize(b, { base: 1024, digits: 'fixed1', numberLocale: null });
|
|
505
631
|
}
|
|
506
632
|
|
|
507
633
|
function expiryLine(days: number): string {
|
|
@@ -514,6 +640,60 @@ function validUntil(r: { expiresAt?: string | null } | null): string {
|
|
|
514
640
|
return validUntilLine(r?.expiresAt ?? null, tr.value ? 'tr' : 'en');
|
|
515
641
|
}
|
|
516
642
|
|
|
643
|
+
/* ── the top tier: one switch, one sentence, one link ──────────────────── */
|
|
644
|
+
|
|
645
|
+
/** The link the header row shows: the one just minted, else the oldest live one. */
|
|
646
|
+
const primaryLink = computed(() => shareResult.value?.url ?? downloadShares.value[0]?.url ?? '');
|
|
647
|
+
const linkOn = computed(() => !!primaryLink.value);
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* The plain sentence. It says who can open the item RIGHT NOW, which is the
|
|
651
|
+
* question the dialog exists to answer.
|
|
652
|
+
*
|
|
653
|
+
* ⚠ The PIN claim is only made about a link this dialog minted: `listShares`
|
|
654
|
+
* does not return `password_pin` (by design — the server will not re-serve a
|
|
655
|
+
* PIN), so for a link that was already there we say how many exist rather than
|
|
656
|
+
* guessing at its protection. A confident wrong sentence about who can read a
|
|
657
|
+
* file is worse than a vaguer true one.
|
|
658
|
+
*/
|
|
659
|
+
const whoLine = computed(() => {
|
|
660
|
+
if (!linkOn.value) return t('access.who.private');
|
|
661
|
+
if (shareResult.value) return shareResult.value.pin ? t('access.who.pin') : t('access.who.link');
|
|
662
|
+
return t('access.who.existing', { n: downloadShares.value.length });
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
/** The muted second line under it: what the server actually stored. */
|
|
666
|
+
const linkDetail = computed(() => {
|
|
667
|
+
const bits: string[] = [];
|
|
668
|
+
if (shareResult.value) {
|
|
669
|
+
bits.push(validUntil(shareResult.value));
|
|
670
|
+
if (shareResult.value.clamped) bits.push(L('(sunucu sınırı uygulandı)', '(server limit applied)'));
|
|
671
|
+
if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
|
|
672
|
+
} else if (downloadShares.value[0]) {
|
|
673
|
+
const s = downloadShares.value[0];
|
|
674
|
+
bits.push(validUntilLine(s.expires_at ?? null, tr.value ? 'tr' : 'en'));
|
|
675
|
+
if (s.max_downloads) bits.push(maxDlOptions.find((o) => o.v === s.max_downloads)?.l ?? String(s.max_downloads));
|
|
676
|
+
}
|
|
677
|
+
return bits.filter(Boolean).join(' · ');
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
/* Section summaries — a named section still has to say what is inside it, or
|
|
681
|
+
the reader has to open all three to find the control they came for. */
|
|
682
|
+
const linkSummary = computed(() => {
|
|
683
|
+
const bits: string[] = [];
|
|
684
|
+
bits.push(sharePwd.value ? t('access.sum.pin_on') : t('access.sum.pin_off'));
|
|
685
|
+
bits.push(expiryLabel(shareExpiry.value));
|
|
686
|
+
if (shareMaxDl.value) bits.push(maxDlOptions.find((o) => o.v === shareMaxDl.value)?.l ?? '');
|
|
687
|
+
return bits.filter(Boolean).join(' · ');
|
|
688
|
+
});
|
|
689
|
+
const peopleSummary = computed(() => {
|
|
690
|
+
const n = direct.value.length + inherited.value.length;
|
|
691
|
+
return n ? t('access.sum.people', { n }) : t('access.sum.people_none');
|
|
692
|
+
});
|
|
693
|
+
const dropSummary = computed(() =>
|
|
694
|
+
dropShares.value.length ? t('access.sum.drop', { n: dropShares.value.length }) : t('access.sum.drop_none'),
|
|
695
|
+
);
|
|
696
|
+
|
|
517
697
|
// Text + title for a download-share link, mirroring shareMailText().
|
|
518
698
|
function shareBody(): { title: string; text: string } {
|
|
519
699
|
const name = pathParts.value.name;
|
|
@@ -573,478 +753,368 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
573
753
|
</script>
|
|
574
754
|
|
|
575
755
|
<template>
|
|
576
|
-
<div class="fx-perm-overlay" @click.self="emit('close')">
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
</
|
|
756
|
+
<div class="fx-perm-overlay fe-share__scrim" @click.self="emit('close')">
|
|
757
|
+
<!-- ⚠ `fx-perm-modal` is kept alongside the new class: e2e/shots/capture.mjs
|
|
758
|
+
and the embedders' own harnesses address the dialog by it. -->
|
|
759
|
+
<div class="fx-perm-modal fe-share" role="dialog" aria-modal="true" :aria-label="t('access.title', { name: pathParts.name })">
|
|
760
|
+
<header class="fe-share__head">
|
|
761
|
+
<span class="fe-share__tile" aria-hidden="true" v-html="titleTile"></span>
|
|
762
|
+
<div class="fe-share__headtext">
|
|
763
|
+
<h3 class="fe-share__title">{{ t('access.title', { name: pathParts.name }) }}</h3>
|
|
764
|
+
<span class="fe-share__path" :title="path">{{ pathParts.adapter }}</span>
|
|
585
765
|
</div>
|
|
586
|
-
<button class="
|
|
766
|
+
<button type="button" class="fe-share__close" :aria-label="t('access.close')" :title="t('access.close')" @click="emit('close')">
|
|
767
|
+
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
768
|
+
</button>
|
|
587
769
|
</header>
|
|
588
770
|
|
|
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.') }}
|
|
771
|
+
<div class="fe-share__body">
|
|
772
|
+
<!-- ───────── tier 1: the link, the whole reason people open this ───── -->
|
|
773
|
+
<div class="fe-share__lead">
|
|
774
|
+
<div class="fe-share__switchrow">
|
|
775
|
+
<span class="fe-share__leadicon" aria-hidden="true" v-html="actionIconSvg('access')"></span>
|
|
776
|
+
<span class="fe-share__leadlabel">{{ t('access.link.switch') }}</span>
|
|
777
|
+
<button
|
|
778
|
+
type="button"
|
|
779
|
+
class="fe-share__switch"
|
|
780
|
+
role="switch"
|
|
781
|
+
:aria-checked="linkOn ? 'true' : 'false'"
|
|
782
|
+
:aria-label="t('access.link.switch')"
|
|
783
|
+
data-testid="share-switch"
|
|
784
|
+
:disabled="shareBusy"
|
|
785
|
+
@click="toggleLink"
|
|
786
|
+
><span class="fe-share__knob"></span></button>
|
|
614
787
|
</div>
|
|
615
|
-
<
|
|
616
|
-
|
|
617
|
-
<template v-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
<
|
|
621
|
-
<
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
788
|
+
<p class="fe-share__who" data-testid="share-who">{{ whoLine }}</p>
|
|
789
|
+
|
|
790
|
+
<template v-if="linkOn">
|
|
791
|
+
<div class="fe-share__linkrow">
|
|
792
|
+
<a :href="primaryLink" target="_blank" rel="noopener" class="fe-share__url" :title="primaryLink">{{ primaryLink }}</a>
|
|
793
|
+
<button type="button" class="fe-share__copy" data-testid="share-copy" @click="copy(primaryLink, 'new')">
|
|
794
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
795
|
+
<span>{{ copied === 'new' ? t('access.copied') : t('access.copy') }}</span>
|
|
796
|
+
</button>
|
|
797
|
+
</div>
|
|
798
|
+
<p v-if="linkDetail" class="fe-share__detail" data-testid="share-valid-until">{{ linkDetail }}</p>
|
|
799
|
+
<div v-if="shareResult?.pin" class="fe-share__pinrow">
|
|
800
|
+
<span class="fe-share__pinlabel">PIN</span>
|
|
801
|
+
<code class="fe-share__pin">{{ shareResult?.pin }}</code>
|
|
802
|
+
<button type="button" class="fe-share__mini" @click="copy(shareResult?.pin ?? '', 'sharepin')">
|
|
803
|
+
{{ copied === 'sharepin' ? t('access.copied') : t('access.copy') }}
|
|
804
|
+
</button>
|
|
805
|
+
</div>
|
|
806
|
+
</template>
|
|
807
|
+
<div v-if="shareErr" class="fe-share__warn">{{ shareErr }}</div>
|
|
808
|
+
</div>
|
|
809
|
+
|
|
810
|
+
<!-- ───────── tier 2: named sections, one click each ───────────────── -->
|
|
811
|
+
<div class="fe-share__sections">
|
|
812
|
+
<!-- Link options: PIN · expiry · download cap · curl · e-mail · links -->
|
|
813
|
+
<section class="fe-share__section">
|
|
814
|
+
<button
|
|
815
|
+
type="button"
|
|
816
|
+
class="fe-share__sechead"
|
|
817
|
+
:aria-expanded="open.link ? 'true' : 'false'"
|
|
818
|
+
data-testid="share-options-toggle"
|
|
819
|
+
@click="toggleSection('link')"
|
|
820
|
+
>
|
|
821
|
+
<svg class="fe-share__caret" :class="{ 'is-open': open.link }" viewBox="0 0 24 24" fill="none"
|
|
822
|
+
stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
|
|
823
|
+
aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
|
|
824
|
+
<span class="fe-share__seclabel">{{ t('access.section.link') }}</span>
|
|
825
|
+
<span class="fe-share__secmeta">{{ linkSummary }}</span>
|
|
826
|
+
</button>
|
|
827
|
+
<div v-if="open.link" class="fe-share__panel">
|
|
828
|
+
<div class="fe-share__opts">
|
|
829
|
+
<label class="fe-share__check">
|
|
830
|
+
<input type="checkbox" v-model="sharePwd" />
|
|
831
|
+
<span>{{ L('PIN ile koru', 'Protect with a PIN') }}</span>
|
|
832
|
+
</label>
|
|
833
|
+
<label class="fe-share__field">
|
|
834
|
+
<span class="fe-share__fieldlabel">{{ L('Süre', 'Expiry') }}</span>
|
|
835
|
+
<select v-model.number="shareExpiry" class="fe-share__select" data-testid="share-expiry">
|
|
836
|
+
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
837
|
+
</select>
|
|
838
|
+
</label>
|
|
839
|
+
<label class="fe-share__field">
|
|
840
|
+
<span class="fe-share__fieldlabel">{{ L('İndirme limiti', 'Download limit') }}</span>
|
|
841
|
+
<select v-model.number="shareMaxDl" class="fe-share__select">
|
|
842
|
+
<option v-for="o in maxDlOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
843
|
+
</select>
|
|
844
|
+
</label>
|
|
845
|
+
</div>
|
|
846
|
+
<p v-if="ttlHint" class="fe-share__hint" data-testid="share-ttl-hint">{{ ttlHint }}</p>
|
|
847
|
+
<button type="button" class="fx-perm-create fe-share__btn fe-share__btn--primary fe-share__btn--wide" :disabled="shareBusy" @click="createLink">
|
|
848
|
+
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
849
|
+
</button>
|
|
850
|
+
|
|
851
|
+
<p v-if="!shareResult && shareMailTo" class="fe-share__hint">
|
|
852
|
+
{{ L('Bir bağlantı oluşturun, ardından', 'Create a link, then it will be sent to') }}
|
|
853
|
+
<strong>{{ shareMailTo }}</strong> {{ L('adresine gönderin.', '.') }}
|
|
854
|
+
</p>
|
|
855
|
+
|
|
856
|
+
<template v-if="shareResult">
|
|
857
|
+
<!-- one-line download command, for pulling the file onto a server -->
|
|
858
|
+
<div class="fx-perm-cli fe-share__cli">
|
|
859
|
+
<span class="fe-share__fieldlabel">{{ L('Komut satırı', 'Command line') }}</span>
|
|
860
|
+
<div class="fe-share__clirow">
|
|
861
|
+
<code class="fe-share__clicmd" :title="shareCli">{{ shareCli }}</code>
|
|
862
|
+
<button type="button" class="fe-share__mini" @click="copy(shareCli, 'sharecli')">
|
|
863
|
+
{{ copied === 'sharecli' ? t('access.copied') : t('access.copy') }}
|
|
864
|
+
</button>
|
|
865
|
+
</div>
|
|
634
866
|
</div>
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
867
|
+
|
|
868
|
+
<!-- send by email (one or more, comma/space separated) -->
|
|
869
|
+
<div class="fe-share__mailrow">
|
|
870
|
+
<input v-model="shareMailTo" type="text" class="fe-share__input" autocomplete="off"
|
|
871
|
+
:placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendShareMail" />
|
|
872
|
+
<button type="button" class="fe-share__btn" :disabled="shareMailBusy" @click="sendShareMail">
|
|
873
|
+
{{ L('Gönder', 'Send') }}
|
|
874
|
+
</button>
|
|
875
|
+
</div>
|
|
876
|
+
<div v-if="shareMailNotice" class="fe-share__notice">{{ shareMailNotice }}</div>
|
|
877
|
+
|
|
878
|
+
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
879
|
+
<button v-if="canShare" type="button" class="fe-share__btn fe-share__btn--wide" @click="nativeShare(shareBody())">
|
|
880
|
+
<span aria-hidden="true" v-html="actionIconSvg('access')"></span>
|
|
881
|
+
<span>{{ L('Paylaş', 'Share') }}</span>
|
|
640
882
|
</button>
|
|
883
|
+
</template>
|
|
884
|
+
|
|
885
|
+
<div class="fe-share__list">
|
|
886
|
+
<h4 class="fe-share__listhead">{{ L('Mevcut bağlantılar', 'Existing links') }}</h4>
|
|
887
|
+
<p v-if="!downloadShares.length" class="fe-share__empty">{{ L('Yok', 'None') }}</p>
|
|
888
|
+
<div v-for="s in downloadShares" :key="s.uuid" class="fe-share__row">
|
|
889
|
+
<span class="fe-share__url" :title="s.url">{{ s.url }}</span>
|
|
890
|
+
<button type="button" class="fe-share__mini" @click="copy(s.url, s.uuid)">
|
|
891
|
+
{{ copied === s.uuid ? t('access.copied') : t('access.copy') }}
|
|
892
|
+
</button>
|
|
893
|
+
<button type="button" class="fe-share__del" :disabled="shareBusy" :title="L('İptal', 'Revoke')"
|
|
894
|
+
:aria-label="L('İptal', 'Revoke')" @click="revoke(s)">
|
|
895
|
+
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
896
|
+
</button>
|
|
897
|
+
</div>
|
|
641
898
|
</div>
|
|
899
|
+
</div>
|
|
900
|
+
</section>
|
|
642
901
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
902
|
+
<!-- People with access: the per-user / inherited grants -->
|
|
903
|
+
<section v-if="canManage" class="fe-share__section">
|
|
904
|
+
<button
|
|
905
|
+
type="button"
|
|
906
|
+
class="fe-share__sechead"
|
|
907
|
+
:aria-expanded="open.people ? 'true' : 'false'"
|
|
908
|
+
data-testid="share-people-toggle"
|
|
909
|
+
@click="toggleSection('people')"
|
|
910
|
+
>
|
|
911
|
+
<svg class="fe-share__caret" :class="{ 'is-open': open.people }" viewBox="0 0 24 24" fill="none"
|
|
912
|
+
stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
|
|
913
|
+
aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
|
|
914
|
+
<span class="fe-share__seclabel">{{ t('access.section.people') }}</span>
|
|
915
|
+
<span class="fe-share__secmeta">{{ peopleSummary }}</span>
|
|
916
|
+
</button>
|
|
917
|
+
<div v-if="open.people" class="fe-share__panel">
|
|
918
|
+
<div v-if="!storageRbac" class="fe-share__warn">
|
|
919
|
+
{{ 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.') }}
|
|
920
|
+
</div>
|
|
921
|
+
<div v-if="err" class="fe-share__warn">{{ err }}</div>
|
|
922
|
+
<p v-if="loading" class="fe-share__hint">{{ L('Yükleniyor…', 'Loading…') }}</p>
|
|
923
|
+
<template v-else>
|
|
924
|
+
<div class="fe-share__add">
|
|
925
|
+
<div class="fe-share__emailwrap">
|
|
926
|
+
<input v-model="email" type="email" class="fe-share__input" autocomplete="off"
|
|
927
|
+
:placeholder="L('İsim veya e-posta', 'Name or email')"
|
|
928
|
+
@input="onEmailInput" @keyup.enter="submitEmail" @focus="onEmailInput" />
|
|
929
|
+
<ul v-if="showSuggest" class="fe-share__suggest">
|
|
930
|
+
<li v-for="u in suggestions" :key="u.id" @mousedown.prevent="pickUser(u)">
|
|
931
|
+
<span class="fe-share__av fe-share__av--sm">{{ (u.display_name || u.email).charAt(0).toUpperCase() }}</span>
|
|
932
|
+
<span class="fe-share__suggesttxt">
|
|
933
|
+
<span class="fe-share__suggestname">{{ u.display_name || u.email }}</span>
|
|
934
|
+
<span class="fe-share__suggestmeta">{{ u.email }} · {{ u.role }}</span>
|
|
935
|
+
</span>
|
|
936
|
+
</li>
|
|
937
|
+
</ul>
|
|
938
|
+
</div>
|
|
939
|
+
<select v-model="level" class="fe-share__select" :title="levels.find(o => o.v === level)?.d">
|
|
940
|
+
<option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
941
|
+
</select>
|
|
942
|
+
<button type="button" class="fe-share__btn fe-share__btn--primary" :disabled="busy" @click="submitEmail">
|
|
943
|
+
{{ L('Ekle', 'Add') }}
|
|
944
|
+
</button>
|
|
945
|
+
</div>
|
|
946
|
+
|
|
947
|
+
<div v-if="noAccount" class="fe-share__invite">
|
|
948
|
+
<p class="fe-share__hint">{{ L('Bu e-postada hesap yok. Ne yapmak istersiniz?', 'No account for this email — what next?') }}</p>
|
|
949
|
+
<div class="fe-share__inviterow">
|
|
950
|
+
<select v-model="createRole" class="fe-share__select">
|
|
648
951
|
<option value="user">{{ L('Kullanıcı', 'User') }}</option>
|
|
649
952
|
<option value="viewer">{{ L('Görüntüleyen', 'Viewer') }}</option>
|
|
650
953
|
</select>
|
|
651
|
-
<button class="
|
|
954
|
+
<button type="button" class="fe-share__btn fe-share__btn--primary" :disabled="busy" @click="inviteCreateUser">
|
|
652
955
|
{{ L('Kullanıcı oluştur + yetki ver', 'Create user + grant') }}
|
|
653
956
|
</button>
|
|
654
957
|
</div>
|
|
655
|
-
<button class="
|
|
958
|
+
<button type="button" class="fe-share__linkbtn" :disabled="busy" @click="gotoShareWithMail">
|
|
656
959
|
{{ L('Sadece paylaşım linki gönder →', 'Just send a share link →') }}
|
|
657
960
|
</button>
|
|
658
961
|
</div>
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
</div>
|
|
664
|
-
</div>
|
|
962
|
+
<div v-if="notice" class="fe-share__notice">{{ notice }}</div>
|
|
963
|
+
<div v-if="inviteResult?.tempPassword" class="fe-share__notice">
|
|
964
|
+
{{ L('Geçici parola:', 'Temp password:') }} <code class="fe-share__pin">{{ inviteResult.tempPassword }}</code>
|
|
965
|
+
</div>
|
|
665
966
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
967
|
+
<div class="fe-share__list">
|
|
968
|
+
<h4 class="fe-share__listhead">{{ L('Erişimi olanlar', 'People with access') }}</h4>
|
|
969
|
+
<p v-if="!direct.length && !inherited.length" class="fe-share__empty">
|
|
970
|
+
{{ L('Henüz kimseyle paylaşılmadı.', 'Not shared with anyone yet.') }}
|
|
971
|
+
</p>
|
|
972
|
+
<div v-for="g in direct" :key="'d' + g.id" class="fe-share__row">
|
|
973
|
+
<span class="fe-share__av">{{ ginitial(g) }}</span>
|
|
974
|
+
<span class="fe-share__person" :title="g.user_email">{{ glabel(g) }}</span>
|
|
975
|
+
<select class="fe-share__select fe-share__select--sm" :value="g.level"
|
|
976
|
+
@change="changeLevel(g, ($event.target as HTMLSelectElement).value)">
|
|
977
|
+
<option v-for="o in levels" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
978
|
+
</select>
|
|
979
|
+
<button type="button" class="fe-share__del" :disabled="busy" :title="L('Kaldır', 'Remove')"
|
|
980
|
+
:aria-label="L('Kaldır', 'Remove')" @click="removeGrant(g)">
|
|
981
|
+
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
982
|
+
</button>
|
|
983
|
+
</div>
|
|
984
|
+
<div v-for="g in inherited" :key="'i' + g.id" class="fe-share__row fe-share__row--dim">
|
|
985
|
+
<span class="fe-share__av fe-share__av--dim">{{ ginitial(g) }}</span>
|
|
986
|
+
<span class="fe-share__person" :title="g.user_email">{{ glabel(g) }}</span>
|
|
987
|
+
<span class="fe-share__badge">{{ levelLabel(g.level) }}</span>
|
|
988
|
+
<span class="fe-share__from" :title="L('Üst klasörden gelir', 'Inherited from') + ': ' + (g.path_prefix || '/')">
|
|
989
|
+
{{ g.path_prefix || '/' }}
|
|
990
|
+
</span>
|
|
991
|
+
</div>
|
|
992
|
+
</div>
|
|
993
|
+
</template>
|
|
689
994
|
</div>
|
|
690
|
-
</
|
|
691
|
-
</template>
|
|
995
|
+
</section>
|
|
692
996
|
|
|
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') }}
|
|
997
|
+
<!-- Request files: the inbound drop link (folders only) -->
|
|
998
|
+
<section v-if="isDir" class="fe-share__section">
|
|
999
|
+
<button
|
|
1000
|
+
type="button"
|
|
1001
|
+
class="fe-share__sechead"
|
|
1002
|
+
:aria-expanded="open.drop ? 'true' : 'false'"
|
|
1003
|
+
data-testid="share-drop-toggle"
|
|
1004
|
+
@click="toggleSection('drop')"
|
|
1005
|
+
>
|
|
1006
|
+
<svg class="fe-share__caret" :class="{ 'is-open': open.drop }" viewBox="0 0 24 24" fill="none"
|
|
1007
|
+
stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
|
|
1008
|
+
aria-hidden="true" focusable="false"><path d="M9 6l6 6-6 6" /></svg>
|
|
1009
|
+
<span class="fe-share__seclabel">{{ t('access.section.drop') }}</span>
|
|
1010
|
+
<span class="fe-share__secmeta">{{ dropSummary }}</span>
|
|
717
1011
|
</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>
|
|
1012
|
+
<div v-if="open.drop" class="fe-share__panel">
|
|
1013
|
+
<p class="fe-share__hint">
|
|
1014
|
+
{{ 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.') }}
|
|
1015
|
+
</p>
|
|
1016
|
+
<div class="fe-share__opts">
|
|
1017
|
+
<label class="fe-share__check">
|
|
1018
|
+
<input type="checkbox" v-model="dropPwd" />
|
|
1019
|
+
<span>{{ L('PIN ile koru', 'Protect with a PIN') }}</span>
|
|
1020
|
+
</label>
|
|
1021
|
+
<label class="fe-share__field">
|
|
1022
|
+
<span class="fe-share__fieldlabel">{{ L('Süre', 'Expiry') }}</span>
|
|
1023
|
+
<select v-model.number="dropExpiry" class="fe-share__select" data-testid="drop-expiry">
|
|
1024
|
+
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
1025
|
+
</select>
|
|
1026
|
+
</label>
|
|
737
1027
|
</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') }}
|
|
1028
|
+
<p v-if="ttlHint" class="fe-share__hint">{{ ttlHint }}</p>
|
|
1029
|
+
<button type="button" class="fe-share__btn fe-share__btn--primary fe-share__btn--wide" data-testid="drop-create" :disabled="dropBusy" @click="createDropLink">
|
|
1030
|
+
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
763
1031
|
</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
1032
|
|
|
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') }}
|
|
1033
|
+
<button type="button" class="fe-share__linkbtn" :aria-expanded="dropShowAdv ? 'true' : 'false'" @click="dropShowAdv = !dropShowAdv">
|
|
1034
|
+
{{ dropShowAdv ? L('Yükleme sınırlarını gizle', 'Hide upload limits') : L('Yükleme sınırları', 'Upload limits') }}
|
|
778
1035
|
</button>
|
|
779
|
-
<
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1036
|
+
<div v-if="dropShowAdv" class="fe-share__adv">
|
|
1037
|
+
<label class="fe-share__advrow">
|
|
1038
|
+
<span class="fe-share__fieldlabel">{{ L('En fazla dosya', 'Max files') }}</span>
|
|
1039
|
+
<input v-model="dropMaxFiles" type="number" min="1" class="fe-share__input fe-share__input--sm" placeholder="20" />
|
|
1040
|
+
</label>
|
|
1041
|
+
<label class="fe-share__advrow">
|
|
1042
|
+
<span class="fe-share__fieldlabel">{{ L('Dosya başı MB', 'MB / file') }}</span>
|
|
1043
|
+
<input v-model="dropMaxSizeMB" type="number" min="1" class="fe-share__input fe-share__input--sm" placeholder="500" />
|
|
1044
|
+
</label>
|
|
1045
|
+
<label class="fe-share__advrow">
|
|
1046
|
+
<span class="fe-share__fieldlabel">{{ L('İzinli türler', 'Allowed types') }}</span>
|
|
1047
|
+
<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)')" />
|
|
1048
|
+
</label>
|
|
1049
|
+
<label class="fe-share__check">
|
|
1050
|
+
<input type="checkbox" v-model="dropAskName" />
|
|
1051
|
+
<span>{{ L('Yükleyenin adını sor', 'Ask uploader name') }}</span>
|
|
1052
|
+
</label>
|
|
1053
|
+
</div>
|
|
783
1054
|
|
|
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>
|
|
1055
|
+
<div v-if="dropErr" class="fe-share__warn">{{ dropErr }}</div>
|
|
806
1056
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
</label>
|
|
827
|
-
</div>
|
|
1057
|
+
<template v-if="dropResult">
|
|
1058
|
+
<div class="fe-share__linkrow">
|
|
1059
|
+
<a :href="dropResult.url" target="_blank" rel="noopener" class="fe-share__url" :title="dropResult.url">{{ dropResult.url }}</a>
|
|
1060
|
+
<button type="button" class="fe-share__copy" data-testid="drop-copy" @click="copy(dropResult.url, 'drop')">
|
|
1061
|
+
<span aria-hidden="true" v-html="actionIconSvg('copy')"></span>
|
|
1062
|
+
<span>{{ copied === 'drop' ? t('access.copied') : t('access.copy') }}</span>
|
|
1063
|
+
</button>
|
|
1064
|
+
</div>
|
|
1065
|
+
<p class="fe-share__detail">
|
|
1066
|
+
{{ validUntil(dropResult) }}
|
|
1067
|
+
<span v-if="dropResult.clamped">{{ L('(sunucu sınırı uygulandı)', '(server limit applied)') }}</span>
|
|
1068
|
+
</p>
|
|
1069
|
+
<div v-if="dropResult.pin" class="fe-share__pinrow">
|
|
1070
|
+
<span class="fe-share__pinlabel">PIN</span>
|
|
1071
|
+
<code class="fe-share__pin">{{ dropResult.pin }}</code>
|
|
1072
|
+
<button type="button" class="fe-share__mini" @click="copy(dropResult.pin, 'droppin')">
|
|
1073
|
+
{{ copied === 'droppin' ? t('access.copied') : t('access.copy') }}
|
|
1074
|
+
</button>
|
|
1075
|
+
</div>
|
|
828
1076
|
|
|
829
|
-
|
|
1077
|
+
<!-- email the upload link to one or more people -->
|
|
1078
|
+
<div class="fe-share__mailrow">
|
|
1079
|
+
<input v-model="dropMailTo" type="text" class="fe-share__input" autocomplete="off"
|
|
1080
|
+
:placeholder="L('e-posta(lar) — virgülle ayırın', 'email(s) — comma separated')" @keyup.enter="sendDropMail" />
|
|
1081
|
+
<button type="button" class="fe-share__btn" :disabled="dropMailBusy" @click="sendDropMail">
|
|
1082
|
+
{{ L('Gönder', 'Send') }}
|
|
1083
|
+
</button>
|
|
1084
|
+
</div>
|
|
1085
|
+
<div v-if="dropMailNotice" class="fe-share__notice">{{ dropMailNotice }}</div>
|
|
830
1086
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
{{ copied === 'drop' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
1087
|
+
<!-- native share (OS share sheet) — same as the fishapp Share button -->
|
|
1088
|
+
<button v-if="canShare" type="button" class="fe-share__btn fe-share__btn--wide" @click="nativeShare(dropBody())">
|
|
1089
|
+
<span aria-hidden="true" v-html="actionIconSvg('access')"></span>
|
|
1090
|
+
<span>{{ L('Paylaş', 'Share') }}</span>
|
|
836
1091
|
</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>
|
|
1092
|
+
</template>
|
|
848
1093
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
<
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1094
|
+
<div class="fe-share__list">
|
|
1095
|
+
<h4 class="fe-share__listhead">{{ L('Mevcut bağlantılar', 'Existing links') }}</h4>
|
|
1096
|
+
<p v-if="!dropShares.length" class="fe-share__empty">{{ L('Yok', 'None') }}</p>
|
|
1097
|
+
<div v-for="s in dropShares" :key="s.uuid" class="fe-share__row">
|
|
1098
|
+
<span class="fe-share__url" :title="s.url">{{ s.url }}</span>
|
|
1099
|
+
<button type="button" class="fe-share__mini" @click="copy(s.url, s.uuid)">
|
|
1100
|
+
{{ copied === s.uuid ? t('access.copied') : t('access.copy') }}
|
|
1101
|
+
</button>
|
|
1102
|
+
<button type="button" class="fe-share__del" :disabled="shareBusy" :title="L('İptal', 'Revoke')"
|
|
1103
|
+
:aria-label="L('İptal', 'Revoke')" @click="revoke(s)">
|
|
1104
|
+
<span aria-hidden="true" v-html="actionIconSvg('close')"></span>
|
|
1105
|
+
</button>
|
|
1106
|
+
</div>
|
|
856
1107
|
</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
1108
|
</div>
|
|
864
|
-
</
|
|
865
|
-
</
|
|
1109
|
+
</section>
|
|
1110
|
+
</div>
|
|
866
1111
|
</div>
|
|
1112
|
+
|
|
1113
|
+
<footer class="fe-share__foot">
|
|
1114
|
+
<button type="button" class="fe-share__btn fe-share__btn--primary" data-testid="share-done" @click="emit('close')">
|
|
1115
|
+
{{ t('access.done') }}
|
|
1116
|
+
</button>
|
|
1117
|
+
</footer>
|
|
867
1118
|
</div>
|
|
868
1119
|
</div>
|
|
869
1120
|
</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>
|