@brftech/filex-core 0.17.0 → 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.
@@ -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-expiry">
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-expiry">
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-expiry">
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
- .fx-perm-share-opts { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
909
- .fx-perm-expiry { display: inline-flex; gap: 6px; align-items: center; font-size: 13px; }
910
- .fx-perm-share-opts .fx-perm-btn--primary { margin-left: auto; }
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
- // The PIN (if any) rides in the querystring HandleDownload accepts
62
- // ?pin=, and -L follows the 302 to the presigned URL for S3 storages.
63
- const cliCommand = computed(() => {
64
- const sh = props.share;
65
- if (!sh) return '';
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>