@brftech/filex-core 0.17.1 → 0.18.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/dist/filex-core.js +4317 -4277
- package/dist/filex-core.js.map +1 -1
- package/dist/filex-core.umd.cjs +42 -42
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/PresenceAvatar.vue +60 -0
- package/src/components/PresenceBar.vue +19 -28
- package/src/lib/realtime.ts +5 -0
- package/src/lib/shareCli.ts +48 -0
- package/src/modals/PermissionsModal.vue +62 -13
- package/src/modals/ShareModal.vue +6 -20
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The one-line download command shown next to a fresh share link.
|
|
2
|
+
//
|
|
3
|
+
// It exists because a link is often made for a SERVER, not a person: the fast
|
|
4
|
+
// way to get the file onto a box is to paste one curl, not to open a browser.
|
|
5
|
+
// The command lived in the old standalone share dialog and was left behind
|
|
6
|
+
// when link creation moved into the "Share / Permissions" panel — so it lives
|
|
7
|
+
// here now, in one place, and both surfaces render the same string.
|
|
8
|
+
|
|
9
|
+
/** Wrap a value in single quotes for safe paste into a POSIX shell — an
|
|
10
|
+
* embedded single quote becomes the standard '\'' dance. */
|
|
11
|
+
export function shQuote(value: string): string {
|
|
12
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ShareCliTarget {
|
|
16
|
+
/** The public /s/<token> URL. */
|
|
17
|
+
url: string;
|
|
18
|
+
/** The PIN, when the link has one (it rides in the querystring — the
|
|
19
|
+
* download endpoint accepts ?pin=). */
|
|
20
|
+
pin?: string | null;
|
|
21
|
+
/** File name, for an explicit -o. Unknown → curl takes the server's
|
|
22
|
+
* Content-Disposition name with -OJ. */
|
|
23
|
+
filename?: string | null;
|
|
24
|
+
/** Folder links serve a browse PAGE at the bare URL; the archive is behind
|
|
25
|
+
* ?zip=wait, which blocks until the ZIP is built and then streams it. */
|
|
26
|
+
isDir?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* shareCliCommand renders the copy-paste curl for a share link.
|
|
31
|
+
*
|
|
32
|
+
* -L is not optional: an S3-backed instance answers with a 302 to a presigned
|
|
33
|
+
* URL, and without it curl saves the redirect page instead of the file.
|
|
34
|
+
*/
|
|
35
|
+
export function shareCliCommand(share: ShareCliTarget | null | undefined): string {
|
|
36
|
+
if (!share?.url) return '';
|
|
37
|
+
let url = share.url;
|
|
38
|
+
const params: string[] = [];
|
|
39
|
+
if (share.isDir) params.push('zip=wait');
|
|
40
|
+
if (share.pin) params.push('pin=' + encodeURIComponent(share.pin));
|
|
41
|
+
if (params.length) url += (url.includes('?') ? '&' : '?') + params.join('&');
|
|
42
|
+
|
|
43
|
+
let target = '-OJ ';
|
|
44
|
+
if (share.filename) {
|
|
45
|
+
target = `-o ${shQuote(share.isDir ? `${share.filename}.zip` : share.filename)} `;
|
|
46
|
+
}
|
|
47
|
+
return `curl -fSL ${target}${shQuote(url)}`;
|
|
48
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
|
|
8
8
|
import type { FileApi, Grant, UserSuggestion } from '../composables/useFileApi';
|
|
9
9
|
import type { ShareInfo } from '../types/FileNode';
|
|
10
|
+
import { shareCliCommand } from '../lib/shareCli';
|
|
10
11
|
|
|
11
12
|
const props = defineProps<{
|
|
12
13
|
api: FileApi;
|
|
@@ -96,6 +97,24 @@ const maxDlOptions = [
|
|
|
96
97
|
{ v: 25, l: L('25 indirme', '25 downloads') },
|
|
97
98
|
];
|
|
98
99
|
|
|
100
|
+
// ⚠ The one-line curl went missing the same way the download cap did: it was
|
|
101
|
+
// part of the old standalone share dialog, and link creation moved here
|
|
102
|
+
// without it. A share link is regularly made FOR a server ("pull this onto the
|
|
103
|
+
// box"), and that reader has no browser — so the command comes back, built by
|
|
104
|
+
// the shared helper both surfaces use.
|
|
105
|
+
const shareCli = computed(() =>
|
|
106
|
+
shareCliCommand(
|
|
107
|
+
shareResult.value
|
|
108
|
+
? {
|
|
109
|
+
url: shareResult.value.url,
|
|
110
|
+
pin: shareResult.value.pin,
|
|
111
|
+
filename: pathParts.value.name,
|
|
112
|
+
isDir: !!props.isDir,
|
|
113
|
+
}
|
|
114
|
+
: null,
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
|
|
99
118
|
// ── file-drop (public upload link) state ──
|
|
100
119
|
const dropPwd = ref(false);
|
|
101
120
|
const dropExpiry = ref(0); // days; 0 = never
|
|
@@ -630,22 +649,22 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
630
649
|
<input type="checkbox" v-model="sharePwd" />
|
|
631
650
|
{{ L('PIN ile koru', 'Protect with a PIN') }}
|
|
632
651
|
</label>
|
|
633
|
-
<label class="fx-perm-
|
|
652
|
+
<label class="fx-perm-field">
|
|
634
653
|
<span class="fx-perm-muted">{{ L('Süre', 'Expiry') }}</span>
|
|
635
654
|
<select v-model.number="shareExpiry" class="fx-perm-sel fx-perm-sel--sm">
|
|
636
655
|
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
637
656
|
</select>
|
|
638
657
|
</label>
|
|
639
|
-
<label class="fx-perm-
|
|
658
|
+
<label class="fx-perm-field">
|
|
640
659
|
<span class="fx-perm-muted">{{ L('İndirme limiti', 'Download limit') }}</span>
|
|
641
660
|
<select v-model.number="shareMaxDl" class="fx-perm-sel fx-perm-sel--sm">
|
|
642
661
|
<option v-for="o in maxDlOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
643
662
|
</select>
|
|
644
663
|
</label>
|
|
645
|
-
<button class="fx-perm-btn fx-perm-btn--primary" :disabled="shareBusy" @click="createLink">
|
|
646
|
-
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
647
|
-
</button>
|
|
648
664
|
</div>
|
|
665
|
+
<button class="fx-perm-btn fx-perm-btn--primary fx-perm-create" :disabled="shareBusy" @click="createLink">
|
|
666
|
+
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
667
|
+
</button>
|
|
649
668
|
|
|
650
669
|
<div v-if="shareErr" class="fx-perm-warn">{{ shareErr }}</div>
|
|
651
670
|
|
|
@@ -663,6 +682,17 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
663
682
|
</button>
|
|
664
683
|
</div>
|
|
665
684
|
|
|
685
|
+
<!-- one-line download command, for pulling the file onto a server -->
|
|
686
|
+
<div class="fx-perm-cli">
|
|
687
|
+
<span class="fx-perm-clilabel">{{ L('Komut satırı', 'Command line') }}</span>
|
|
688
|
+
<div class="fx-perm-clirow">
|
|
689
|
+
<code class="fx-perm-clicmd" :title="shareCli">{{ shareCli }}</code>
|
|
690
|
+
<button class="fx-perm-btn fx-perm-btn--sm" @click="copy(shareCli, 'sharecli')">
|
|
691
|
+
{{ copied === 'sharecli' ? L('Kopyalandı ✓', 'Copied ✓') : L('Kopyala', 'Copy') }}
|
|
692
|
+
</button>
|
|
693
|
+
</div>
|
|
694
|
+
</div>
|
|
695
|
+
|
|
666
696
|
<!-- send by email (one or more, comma/space separated) -->
|
|
667
697
|
<div class="fx-perm-mailrow">
|
|
668
698
|
<input v-model="shareMailTo" type="text" class="fx-perm-input" autocomplete="off"
|
|
@@ -708,16 +738,16 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
708
738
|
<input type="checkbox" v-model="dropPwd" />
|
|
709
739
|
{{ L('PIN ile koru', 'Protect with a PIN') }}
|
|
710
740
|
</label>
|
|
711
|
-
<label class="fx-perm-
|
|
741
|
+
<label class="fx-perm-field">
|
|
712
742
|
<span class="fx-perm-muted">{{ L('Süre', 'Expiry') }}</span>
|
|
713
743
|
<select v-model.number="dropExpiry" class="fx-perm-sel fx-perm-sel--sm">
|
|
714
744
|
<option v-for="o in expiryOptions" :key="o.v" :value="o.v">{{ o.l }}</option>
|
|
715
745
|
</select>
|
|
716
746
|
</label>
|
|
717
|
-
<button class="fx-perm-btn fx-perm-btn--primary" :disabled="dropBusy" @click="createDropLink">
|
|
718
|
-
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
719
|
-
</button>
|
|
720
747
|
</div>
|
|
748
|
+
<button class="fx-perm-btn fx-perm-btn--primary fx-perm-create" :disabled="dropBusy" @click="createDropLink">
|
|
749
|
+
{{ L('Bağlantı oluştur', 'Create link') }}
|
|
750
|
+
</button>
|
|
721
751
|
|
|
722
752
|
<button type="button" class="fx-perm-btn fx-perm-btn--ghost fx-perm-advtoggle" @click="dropShowAdv = !dropShowAdv">
|
|
723
753
|
{{ dropShowAdv ? L('Gelişmiş ▲', 'Advanced ▲') : L('Gelişmiş ▼', 'Advanced ▼') }}
|
|
@@ -904,16 +934,35 @@ async function nativeShare(body: { title: string; text: string }) {
|
|
|
904
934
|
.fx-perm-invite-row { display: flex; gap: 8px; align-items: stretch; }
|
|
905
935
|
.fx-perm-invite-row .fx-perm-btn--primary { flex: 1; }
|
|
906
936
|
|
|
907
|
-
/* share result
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
937
|
+
/* share options + result
|
|
938
|
+
⚠ These options used to be one wrapping flex row with the primary button
|
|
939
|
+
pushed to its right end by `margin-left: auto`. That works with two controls
|
|
940
|
+
and breaks with three: at the modal's 520px the row wraps and the button
|
|
941
|
+
drops onto a line of its own, right-aligned and looking dislodged — which is
|
|
942
|
+
exactly what adding the download limit did. A two-column grid of fields with
|
|
943
|
+
the action underneath holds its shape whatever we add next. */
|
|
944
|
+
.fx-perm-share-opts { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 12px; align-items: end; }
|
|
945
|
+
.fx-perm-share-opts > .fx-perm-check { grid-column: 1 / -1; }
|
|
946
|
+
.fx-perm-field { display: flex; flex-direction: column; gap: 4px; font-size: 13px; min-width: 0; }
|
|
947
|
+
.fx-perm-field .fx-perm-sel { width: 100%; }
|
|
948
|
+
.fx-perm-create { width: 100%; margin-top: 10px; }
|
|
949
|
+
@media (max-width: 380px) { .fx-perm-share-opts { grid-template-columns: 1fr; } }
|
|
911
950
|
.fx-perm-result { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--fe-border); }
|
|
912
951
|
.fx-perm-linkrow { display: flex; gap: 8px; align-items: center; }
|
|
913
952
|
.fx-perm-link { color: var(--fe-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; text-decoration: none; }
|
|
914
953
|
.fx-perm-link:hover { text-decoration: underline; }
|
|
915
954
|
.fx-perm-pin { margin: 8px 0 0; font-size: 13px; color: var(--fe-text); display: flex; align-items: center; gap: 8px; }
|
|
916
955
|
.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; }
|
|
956
|
+
/* one-line curl for the fresh link */
|
|
957
|
+
.fx-perm-cli { margin-top: 10px; }
|
|
958
|
+
.fx-perm-clilabel { display: block; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fe-text-muted); margin-bottom: 4px; }
|
|
959
|
+
.fx-perm-clirow { display: flex; gap: 8px; align-items: center; }
|
|
960
|
+
.fx-perm-clicmd {
|
|
961
|
+
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
962
|
+
font-family: var(--fe-font-mono, monospace); font-size: 12px;
|
|
963
|
+
background: var(--fe-bg-hover); color: var(--fe-text);
|
|
964
|
+
padding: 6px 8px; border-radius: var(--fe-radius-sm, 7px);
|
|
965
|
+
}
|
|
917
966
|
.fx-perm-mailrow { display: flex; gap: 8px; margin-top: 10px; }
|
|
918
967
|
.fx-perm-mailrow .fx-perm-input { flex: 1; }
|
|
919
968
|
.fx-perm-hint { font-size: 13px; color: var(--fe-text-muted); margin: 10px 0 0; }
|
|
@@ -3,6 +3,7 @@ import { computed, ref, watch } from 'vue';
|
|
|
3
3
|
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
4
4
|
import type { ShareInfo } from '../types/FileNode';
|
|
5
5
|
import { useLocale } from '../composables/useLocale';
|
|
6
|
+
import { shareCliCommand } from '../lib/shareCli';
|
|
6
7
|
import Modal from './Modal.vue';
|
|
7
8
|
|
|
8
9
|
const props = defineProps<{
|
|
@@ -51,27 +52,12 @@ async function copy(value: string, toast: string) {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
// Wrap a value in single quotes for safe paste into a POSIX shell —
|
|
55
|
-
// embedded single quotes become the standard '\'' dance.
|
|
56
|
-
function shQuote(value: string): string {
|
|
57
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
55
|
// One-line curl that downloads the shared file straight onto a server.
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
const cliCommand = computed(() =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
let url = sh.url;
|
|
67
|
-
if (sh.password_pin) {
|
|
68
|
-
url += (url.includes('?') ? '&' : '?') + 'pin=' + encodeURIComponent(sh.password_pin);
|
|
69
|
-
}
|
|
70
|
-
// Prefer an explicit output name; fall back to the server's
|
|
71
|
-
// Content-Disposition filename (-OJ) when we don't know it.
|
|
72
|
-
const target = sh.filename ? `-o ${shQuote(sh.filename)} ` : '-OJ ';
|
|
73
|
-
return `curl -fSL ${target}${shQuote(url)}`;
|
|
74
|
-
});
|
|
56
|
+
// Built by the shared helper so this dialog and the "Share / Permissions"
|
|
57
|
+
// panel cannot drift into two different commands.
|
|
58
|
+
const cliCommand = computed(() =>
|
|
59
|
+
shareCliCommand(props.share ? { url: props.share.url, pin: props.share.password_pin, filename: props.share.filename } : null),
|
|
60
|
+
);
|
|
75
61
|
</script>
|
|
76
62
|
|
|
77
63
|
<template>
|