@sierra-95/svelte-scaffold 1.0.8 → 1.0.10

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.
Files changed (26) hide show
  1. package/dist/Hooks/preview.d.ts +3 -1
  2. package/dist/Hooks/preview.js +11 -4
  3. package/dist/components/Core/Form/Input/FileInput/fileInput.svelte +61 -33
  4. package/dist/components/Core/Form/Input/FileInput/preview.svelte +28 -15
  5. package/dist/components/Core/Menus/DropdownContainer/dropdown.svelte +2 -2
  6. package/dist/components/Core/others/Previews/Audio/audio.svelte +1 -1
  7. package/dist/components/Core/others/Previews/Document/documents.svelte +14 -2
  8. package/dist/components/Core/others/Previews/GenericFile/genericFile.svelte +26 -0
  9. package/dist/components/Core/others/Previews/GenericFile/genericFile.svelte.d.ts +22 -0
  10. package/dist/components/Core/others/Previews/Image/image.svelte +1 -1
  11. package/dist/components/Core/others/Previews/Video/video.svelte +1 -1
  12. package/dist/components/Modules/Editor/Nodes/Images/images.svelte +1 -1
  13. package/dist/components/Modules/FilePicker/cloudStore.svelte +11 -7
  14. package/dist/components/Modules/FilePicker/controls.svelte +58 -13
  15. package/dist/components/Modules/FilePicker/controls.svelte.d.ts +4 -19
  16. package/dist/components/Modules/FilePicker/filePicker.svelte +3 -4
  17. package/dist/components/Modules/FilePicker/file_properties.svelte +33 -0
  18. package/dist/components/Modules/FilePicker/file_properties.svelte.d.ts +11 -0
  19. package/dist/components/Modules/FilePicker/previews.svelte +5 -1
  20. package/dist/components/Modules/Layout/main.svelte +1 -13
  21. package/dist/global.css +5 -0
  22. package/dist/index.d.ts +3 -1
  23. package/dist/index.js +2 -1
  24. package/dist/stores/modules/fileInput.d.ts +14 -7
  25. package/dist/stores/modules/fileInput.js +2 -2
  26. package/package.json +1 -1
@@ -1,3 +1,5 @@
1
+ import type { FileInputStoreMediaItem } from '../index.js';
1
2
  export declare function getPreviewUrlForMedia(file: File): string;
2
- export declare function toggleSelectForMedia(id: string, url: string, urlsOnly: boolean): void;
3
+ export declare function toggleSelectForMedia(item: FileInputStoreMediaItem, urlsOnly: boolean): void;
3
4
  export declare function removeFileForMedia(index: number, e: Event): void;
5
+ export declare const DOCUMENT_MIME_TYPES: Set<string>;
@@ -2,16 +2,16 @@ import { fileInputStore } from '../index.js';
2
2
  export function getPreviewUrlForMedia(file) {
3
3
  return URL.createObjectURL(file);
4
4
  }
5
- export function toggleSelectForMedia(id, url, urlsOnly) {
5
+ export function toggleSelectForMedia(item, urlsOnly) {
6
6
  if (!urlsOnly)
7
7
  return;
8
8
  fileInputStore.update(state => {
9
- const index = state.submissions.findIndex(item => item.id === id);
9
+ const index = state.submissions.findIndex(sub => sub.id === item.id);
10
10
  return {
11
11
  ...state,
12
12
  submissions: index === -1
13
- ? [...state.submissions, { id, url }] // select
14
- : state.submissions.filter(item => item.id !== id) // deselect
13
+ ? [...state.submissions, item] // select full media
14
+ : state.submissions.filter(sub => sub.id !== item.id) // deselect
15
15
  };
16
16
  });
17
17
  }
@@ -22,3 +22,10 @@ export function removeFileForMedia(index, e) {
22
22
  selectedFiles: state.selectedFiles.filter((_, i) => i !== index)
23
23
  }));
24
24
  }
25
+ export const DOCUMENT_MIME_TYPES = new Set([
26
+ 'application/pdf',
27
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
28
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
29
+ 'application/vnd.ms-excel',
30
+ 'text/plain'
31
+ ]);
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy } from 'svelte';
3
- import {fileInputStore, Button, setToastMessage} from '../../../../../index.js'
3
+ import {fileInputStore, Button, setToastMessage, DOCUMENT_MIME_TYPES} from '../../../../../index.js'
4
4
  import Preview from './preview.svelte';
5
5
 
6
6
  const {
@@ -11,74 +11,97 @@
11
11
 
12
12
  let fileInput: HTMLInputElement;
13
13
 
14
- const typeMap: Record<'image' | 'audio' | 'video' | 'pdf', string[]> = {
14
+ const typeMap: Record<'image' | 'audio' | 'video' | 'documents' | 'others', string[]> = {
15
15
  image: ['image/'],
16
16
  audio: ['audio/'],
17
17
  video: ['video/'],
18
- pdf: ['application/pdf']
18
+ documents: Array.from(DOCUMENT_MIME_TYPES),
19
+ others: []
19
20
  };
20
21
 
21
-
22
22
  //Validate and add files
23
23
  function isAllowedType(file: File): boolean {
24
- return $fileInputStore.uploadType.some(type =>
25
- typeMap[type].some(prefix => file.type.startsWith(prefix))
26
- );
24
+ return $fileInputStore.uploadType.some(type => {
25
+ if (type === 'others') return true;
26
+ const prefixes = typeMap[type];
27
+ return prefixes.some(prefix => file.type.startsWith(prefix));
28
+ });
27
29
  }
28
30
 
29
- function validateAndAddFile(file: File) {
30
- if (!isAllowedType(file)) {
31
- setToastMessage('error', `"${file.name}" is not an allowed file type`);
32
- return;
33
- }
31
+ function allowedTypes(){
32
+ const allowedTypes = $fileInputStore.uploadType
33
+ .filter(t => t !== 'others')
34
+ .map(t => {
35
+ if (t === 'documents') return 'Documents (PDF, Word, Excel, TXT)';
36
+ return t.charAt(0).toUpperCase() + t.slice(1);
37
+ })
38
+ .join(', ');
39
+ return allowedTypes;
40
+ }
34
41
 
35
- if (file.size > $fileInputStore.sizeConstraint) {
36
- setToastMessage('error', `"${file.name}" exceeds the allowed file size`);
37
- return;
42
+ function processFiles(files: FileList | File[]) {
43
+ const validFiles: File[] = [];
44
+
45
+ for (const file of files) {
46
+ if (!isAllowedType(file)) {
47
+ setToastMessage(
48
+ 'error',
49
+ `"${file.name}" is not an allowed file type. Allowed types: ${allowedTypes()}`
50
+ );
51
+ continue; // skip invalid
52
+ }
53
+
54
+ if (file.size > $fileInputStore.sizeConstraint) {
55
+ setToastMessage(
56
+ 'error',
57
+ `"${file.name}" exceeds the allowed file size`
58
+ );
59
+ continue; // skip oversized
60
+ }
61
+ validFiles.push(file);
38
62
  }
39
63
 
40
- fileInputStore.update(state => ({
41
- ...state,
42
- selectedFiles: [...state.selectedFiles, file]
43
- }));
64
+ if (validFiles.length > 0) {
65
+ fileInputStore.update(state => ({
66
+ ...state,
67
+ selectedFiles: [...state.selectedFiles, ...validFiles]
68
+ }));
69
+ }
70
+ clearInput();
44
71
  }
45
72
 
46
73
 
74
+
47
75
  //Handle file selection via input
48
76
  function handleFileChange(event: Event) {
49
77
  const files = (event.target as HTMLInputElement).files;
50
78
  if (!files || files.length === 0) return;
51
-
52
- for (const file of files) {
53
- validateAndAddFile(file);
54
- }
79
+ processFiles(files);
55
80
  }
56
81
 
57
82
  //Handle drag and drop
58
83
  function handleDrop(event: DragEvent) {
59
84
  event.preventDefault();
60
85
  if (!event.dataTransfer?.files.length) return;
61
-
62
- for (const file of event.dataTransfer.files) {
63
- validateAndAddFile(file);
64
- }
65
-
86
+ processFiles(event.dataTransfer.files);
66
87
  }
67
88
 
68
89
  function handleDragOver(event: DragEvent) {
69
90
  event.preventDefault();
70
91
  }
71
92
 
93
+ function clearInput() {
94
+ if (fileInput) {
95
+ fileInput.value = '';
96
+ }
97
+ }
72
98
  //Delete selected files
73
99
  function handleClear() {
74
100
  fileInputStore.update(state => ({
75
101
  ...state,
76
102
  selectedFiles: []
77
103
  }));
78
- if (fileInput) {
79
- fileInput.value = '';
80
- }
81
-
104
+ clearInput();
82
105
  }
83
106
 
84
107
  onDestroy(() => {
@@ -92,7 +115,12 @@
92
115
  bind:this={fileInput}
93
116
  type="file"
94
117
  accept={$fileInputStore.uploadType
95
- .map(t => (t === 'pdf' ? 'application/pdf' : `${t}/*`))
118
+ .map(t => {
119
+ if (t === 'documents') return Array.from(DOCUMENT_MIME_TYPES).join(',');
120
+ if (t === 'others') return '';
121
+ return `${t}/*`;
122
+ })
123
+ .filter(Boolean)
96
124
  .join(',')
97
125
  }
98
126
  hidden
@@ -1,21 +1,30 @@
1
1
  <script lang="ts">
2
- import {fileInputStore, PreviewAudio,PreviewDocument,PreviewImage,PreviewVideo} from '../../../../../index.js';
2
+ import {fileInputStore, PreviewAudio,PreviewDocument,PreviewImage,PreviewVideo, PreviewGenericFile, DOCUMENT_MIME_TYPES} from '../../../../../index.js';
3
3
 
4
+ function getMediaCategory(file: File):
5
+ 'Audio' | 'Documents' | 'Images' | 'Videos' | 'Others' {
4
6
 
5
- $: media = {
6
- Audio: $fileInputStore.selectedFiles.filter(file =>
7
- file.type.startsWith('audio/')
8
- ),
9
- Documents: $fileInputStore.selectedFiles.filter(
10
- file => file.type === 'application/pdf'
11
- ),
12
- Images: $fileInputStore.selectedFiles.filter(file =>
13
- file.type.startsWith('image/')
14
- ),
15
- Videos: $fileInputStore.selectedFiles.filter(file =>
16
- file.type.startsWith('video/')
17
- )
18
- };
7
+ if (file.type.startsWith('audio/')) return 'Audio';
8
+ if (file.type.startsWith('image/')) return 'Images';
9
+ if (file.type.startsWith('video/')) return 'Videos';
10
+ if (DOCUMENT_MIME_TYPES.has(file.type)) return 'Documents';
11
+
12
+ return 'Others';
13
+ }
14
+
15
+ $: media = $fileInputStore.selectedFiles.reduce(
16
+ (acc, file) => {
17
+ acc[getMediaCategory(file)].push(file);
18
+ return acc;
19
+ },
20
+ {
21
+ Audio: [] as File[],
22
+ Documents: [] as File[],
23
+ Images: [] as File[],
24
+ Videos: [] as File[],
25
+ Others: [] as File[],
26
+ }
27
+ );
19
28
  </script>
20
29
 
21
30
  <div style="max-height: 300px; overflow-y: auto; padding: 0.5rem;">
@@ -35,4 +44,8 @@
35
44
  <h3 style="margin: 1rem 0rem;">Videos</h3>
36
45
  <PreviewVideo buttonTimes urlsOnly={false} {media}/>
37
46
  {/if}
47
+ {#if media.Others.length > 0}
48
+ <h3 style="margin: 1rem 0rem;">Other Files</h3>
49
+ <PreviewGenericFile buttonTimes urlsOnly={false} {media}/>
50
+ {/if}
38
51
  </div>
@@ -49,10 +49,10 @@
49
49
  };
50
50
  if (browser) {
51
51
  onMount(() => {
52
- document.addEventListener('click', handleDocumentClick);
52
+ document.addEventListener('pointerdown', handleDocumentClick, true);
53
53
  });
54
54
  onDestroy(() => {
55
- document.removeEventListener('click', handleDocumentClick);
55
+ document.removeEventListener('pointerdown', handleDocumentClick, true);
56
56
  });
57
57
  }
58
58
  </script>
@@ -42,7 +42,7 @@
42
42
  {:else}
43
43
  <div style="display: flex; flex-wrap:wrap; gap: 1rem">
44
44
  {#each media.Audio as item (item.id || uuid())}
45
- <div on:click={() => toggleSelectForMedia(item.id,item.url,urlsOnly)} role="none" class="sierra-translate" style="position: relative; width: 200px;display: flex; gap: 0.5rem; padding: 0.5rem; box-shadow: var(--box-shadow-secondary); border-radius: 0.3rem; cursor: pointer;">
45
+ <div on:click={() => toggleSelectForMedia(item, urlsOnly)} role="none" class="sierra-translate" style="position: relative; width: 200px;display: flex; gap: 0.5rem; padding: 0.5rem; box-shadow: var(--box-shadow-secondary); border-radius: 0.3rem; cursor: pointer;">
46
46
  <button aria-label="Play or pause audio" on:click|stopPropagation={() => togglePlay(item.id)}>
47
47
  <i class="fa {audioPlaying[item.id] ? 'fa-pause' : 'fa-play'}" aria-hidden="true"></i>
48
48
  </button>
@@ -3,13 +3,25 @@
3
3
  export let media;
4
4
  export let buttonTimes = false;
5
5
  export let urlsOnly = true;
6
+ const iconBaseUrl = 'https://files.michaelmachohi.com/svelte-scaffold/icons/';
7
+ const mimeTypeToIcon: Record<string, string> = {
8
+ 'application/pdf': 'pdf.icon.png',
9
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'word.icon.png',
10
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'excel.icon.png',
11
+ 'application/vnd.ms-excel': 'excel.icon.png',
12
+ 'text/plain': 'txt.icon.png'
13
+ };
14
+ const getIconUrl = (mimeType: string) => {
15
+ return iconBaseUrl + (mimeTypeToIcon[mimeType] || 'generic.icon.png');
16
+ };
6
17
  </script>
7
18
  {#if media.Documents.length === 0}
8
19
  <p>No documents available.</p>
9
20
  {:else}
10
21
  <div style="display: flex; flex-wrap: wrap; gap: 1rem;">
11
22
  {#each media.Documents as item}
12
- <div on:click={() => toggleSelectForMedia(item.id,item.url,urlsOnly)} role="none" class="sierra-translate" style="position: relative; width: 200px; padding: 0.5rem; box-shadow: var(--box-shadow-secondary); cursor: pointer;">
23
+ <div on:click={() => toggleSelectForMedia(item, urlsOnly)} role="none" class="sierra-translate" style="position: relative; width: 70px; cursor: pointer;">
24
+ <img src={getIconUrl(item.type || item.mime_type)} alt="Document Icon" style="height: 70px;"/>
13
25
  {#if buttonTimes}
14
26
  <ButtonTimes vertical="bottom" onclick={(e: Event) => removeFileForMedia(
15
27
  $fileInputStore.selectedFiles.indexOf(item), e
@@ -18,7 +30,7 @@
18
30
  {#if $fileInputStore.submissions.some(sub => sub.url === item.url || sub.id === item.id)}
19
31
  <ButtonSelect />
20
32
  {/if}
21
- <h3 class="sierra-text-ellipsis">{item.original_name || item.name}</h3>
33
+ <h3 class="sierra-text-wrap">{item.original_name || item.name}</h3>
22
34
  </div>
23
35
  {/each}
24
36
  </div>
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import {ButtonTimes,ButtonSelect, fileInputStore, removeFileForMedia, toggleSelectForMedia} from '../../../../../index.js'
3
+ export let media;
4
+ export let buttonTimes = false;
5
+ export let urlsOnly = true;
6
+ </script>
7
+ {#if media.Others.length === 0}
8
+ <p>No other files available.</p>
9
+ {:else}
10
+ <div style="display: flex; flex-wrap: wrap; gap: 1rem;">
11
+ {#each media.Others as item}
12
+ <div on:click={() => toggleSelectForMedia(item, urlsOnly)} role="none" class="sierra-translate" style="position: relative; width: 70px; cursor: pointer;">
13
+ <img src="https://files.michaelmachohi.com/svelte-scaffold/icons/generic.icon.png" alt="Generic File Icon" style="height: 70px;"/>
14
+ {#if buttonTimes}
15
+ <ButtonTimes vertical="bottom" onclick={(e: Event) => removeFileForMedia(
16
+ $fileInputStore.selectedFiles.indexOf(item), e
17
+ )}/>
18
+ {/if}
19
+ {#if $fileInputStore.submissions.some(sub => sub.url === item.url || sub.id === item.id)}
20
+ <ButtonSelect />
21
+ {/if}
22
+ <h3 class="sierra-text-wrap">{item.original_name || item.name}</h3>
23
+ </div>
24
+ {/each}
25
+ </div>
26
+ {/if}
@@ -0,0 +1,22 @@
1
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
+ $$bindings?: Bindings;
4
+ } & Exports;
5
+ (internal: unknown, props: Props & {
6
+ $$events?: Events;
7
+ $$slots?: Slots;
8
+ }): Exports & {
9
+ $set?: any;
10
+ $on?: any;
11
+ };
12
+ z_$$bindings?: Bindings;
13
+ }
14
+ declare const GenericFile: $$__sveltets_2_IsomorphicComponent<{
15
+ media: any;
16
+ buttonTimes?: boolean;
17
+ urlsOnly?: boolean;
18
+ }, {
19
+ [evt: string]: CustomEvent<any>;
20
+ }, {}, {}, string>;
21
+ type GenericFile = InstanceType<typeof GenericFile>;
22
+ export default GenericFile;
@@ -10,7 +10,7 @@
10
10
  {:else}
11
11
  <div style="display: flex; flex-wrap: wrap; gap: 1rem;">
12
12
  {#each media.Images as item}
13
- <div on:click={() => toggleSelectForMedia(item.id,item.url,urlsOnly)} role="none" class="sierra-translate" style="position:relative; box-shadow: var(--box-shadow-secondary);width: 150px;border-radius: 0.3rem;cursor: pointer;">
13
+ <div on:click={() => toggleSelectForMedia(item, urlsOnly)} role="none" class="sierra-translate" style="position:relative; box-shadow: var(--box-shadow-secondary);width: 150px;border-radius: 0.3rem;cursor: pointer;">
14
14
  <img src={urlsOnly ? item.url : getPreviewUrlForMedia(item)} alt={item} style="width: 100%; height: 100px; object-fit: cover;border-top-left-radius: 0.3rem; border-top-right-radius: 0.3rem;" />
15
15
  <h3 style="margin: 0.5rem" class="sierra-text-ellipsis">{item.original_name || item.name}</h3>
16
16
  {#if buttonTimes}
@@ -10,7 +10,7 @@
10
10
  {:else}
11
11
  <div style="display: flex; flex-wrap: wrap; gap: 1rem;">
12
12
  {#each media.Videos as item}
13
- <div on:click={() => toggleSelectForMedia(item.id,item.url,urlsOnly)} role="none" class="sierra-translate" style="position:relative; box-shadow: var(--box-shadow-secondary);width: 150px;border-radius: 0.3rem; cursor: pointer;">
13
+ <div on:click={() => toggleSelectForMedia(item, urlsOnly)} role="none" class="sierra-translate" style="position:relative; box-shadow: var(--box-shadow-secondary);width: 150px;border-radius: 0.3rem; cursor: pointer;">
14
14
  <video on:click|stopPropagation src={urlsOnly ? item.url : getPreviewUrlForMedia(item)} controls style="width: 100%; height: 100px; object-fit: cover; border-top-left-radius: 0.3rem; border-top-right-radius: 0.3rem;">
15
15
  <track kind="captions" />
16
16
  </video>
@@ -20,7 +20,7 @@
20
20
  fileInputStore.update(store => ({
21
21
  ...store,
22
22
  uploadType: ['image'],
23
- disabledMenuItem: ['Music', 'Videos', 'Documents'],
23
+ disabledMenuItem: ['Music', 'Videos', 'Documents', 'Others'],
24
24
  r2_key: $editorStore.r2_key,
25
25
  serverGetUrl: $editorStore.serverGetUrl,
26
26
  serverUploadUrl: $editorStore.serverUploadUrl,
@@ -1,14 +1,15 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
- import { fileInputStore, User, isLoggedIn, setToastMessage, buttonRipple } from '../../../index.js';
3
+ import { fileInputStore, User, setToastMessage, buttonRipple, isMobile } from '../../../index.js';
4
4
  import Previews from './previews.svelte';
5
5
 
6
6
  export let processing: boolean;
7
- const tabs: { name: 'Documents'| 'Music' | 'Pictures' | 'Videos'; icon: string }[] = [
7
+ const tabs: { name: 'Documents'| 'Music' | 'Pictures' | 'Videos'| 'Others'; icon: string }[] = [
8
8
  { name: 'Documents', icon: 'fa fa-file-o' },
9
9
  { name: 'Music', icon: 'fa fa-music' },
10
10
  { name: 'Pictures', icon: 'fa fa-file-image-o' },
11
- { name: 'Videos', icon: 'fa fa-film' }
11
+ { name: 'Videos', icon: 'fa fa-film' },
12
+ { name: 'Others', icon: 'fa fa-folder-o' }
12
13
  ];
13
14
 
14
15
  $: if ($fileInputStore.disabledMenuItem?.includes($fileInputStore.activeMenu)) {
@@ -25,6 +26,7 @@
25
26
  Documents: MediaItem[];
26
27
  Images: MediaItem[];
27
28
  Videos: MediaItem[];
29
+ Others: MediaItem[];
28
30
  };
29
31
 
30
32
  let media: MediaResponse | null = null;
@@ -39,7 +41,6 @@
39
41
  try {
40
42
  processing = true;
41
43
  const params = new URLSearchParams({
42
- isLoggedIn: String($isLoggedIn),
43
44
  userId: $User.userId
44
45
  });
45
46
 
@@ -75,7 +76,7 @@
75
76
  }
76
77
  </style>
77
78
  <main id="sierra-cloud-store" style="display: flex; gap: 1rem; height: 100%; min-height: 300px;">
78
- <nav style="display: flex; flex-direction: column; gap: 0.5rem; padding-top: 1rem; background-color: var(--background); border-bottom-left-radius:5px;">
79
+ <nav style="display: flex; flex-direction: column; gap: {$isMobile ? '1rem' : '0.5rem'}; padding-top: 1rem; background-color: var(--background); border-bottom-left-radius:5px;">
79
80
  {#each tabs as tab}
80
81
  <button
81
82
  use:buttonRipple
@@ -85,11 +86,14 @@
85
86
  activeMenu: tab.name
86
87
  }))
87
88
  }
88
- style="width:100%; padding: 0.1rem 0.5rem; {$fileInputStore.disabledMenuItem?.includes(tab.name) ? 'display: none;' : 'display: flex;align-items: center; gap: 0.5rem;'}"
89
+ style="
90
+ width:100%; padding: 0.1rem 0.5rem;
91
+ {$fileInputStore.disabledMenuItem?.includes(tab.name) ? 'display: none;' : 'display: flex;align-items: center; gap: 0.5rem;'}
92
+ "
89
93
  class={`sierra-text-ellipsis ${$fileInputStore.activeMenu === tab.name ? 'icon-active' : ''}`}
90
94
  >
91
95
  <i class={tab.icon} style="font-size: 10px; color: var(--icon-theme);"></i>
92
- {tab.name}
96
+ {$isMobile ? '' : tab.name}
93
97
  </button>
94
98
  {/each}
95
99
  </nav>
@@ -1,7 +1,11 @@
1
1
  <script lang="ts">
2
- import {fileInputStore, LinearProgress, setToastMessage, modalStore, resetFileInputStore} from "../../../index.js";
2
+ import {fileInputStore, LinearProgress, setToastMessage, modalStore, resetFileInputStore, DropdownContainer, MenuItem, buttonRipple} from "../../../index.js";
3
3
  import { get } from 'svelte/store';
4
- export let processing:boolean;
4
+ import FileProperties from "./file_properties.svelte";
5
+
6
+ let {
7
+ processing,
8
+ } = $props();
5
9
 
6
10
  const menuItems = [
7
11
  {
@@ -21,7 +25,38 @@
21
25
  uploadModalOpen: false
22
26
  }));
23
27
  }
28
+
29
+ const iconSize = '15px';
30
+ let showProperties = $state(false);
31
+ let openActionsMenu = $state(false);
32
+ let disableActions = $state(true);
33
+ $effect(() => {
34
+ if($fileInputStore.submissions.length > 0 && $fileInputStore.activeTab === 'cloud'){
35
+ disableActions = false;
36
+ } else {
37
+ disableActions = true;
38
+ }
39
+ });
40
+
41
+ function handleDownload() {
42
+ const store = get(fileInputStore);
43
+ if (!store.submissions || store.submissions.length === 0) return;
44
+
45
+ store.submissions.forEach(item => {
46
+ downloadFile(item.url, item.id);
47
+ });
48
+ }
49
+ function downloadFile(url: string, filename?: string) {
50
+ const a = document.createElement('a');
51
+ a.href = url;
52
+ if (filename) a.download = filename;
53
+ document.body.appendChild(a);
54
+ a.click();
55
+ document.body.removeChild(a);
56
+ }
57
+
24
58
  function handleDelete() {
59
+ openActionsMenu = false;
25
60
  modalStore.update(store => ({
26
61
  ...store,
27
62
  open: true,
@@ -60,9 +95,9 @@
60
95
  return;
61
96
  }
62
97
 
63
- data.forEach((item: { id: string; status: string; code: number }) => {
98
+ data.forEach((item: { id: string; code: number }) => {
64
99
  if (item.code === 404) {
65
- setToastMessage('error', `Failed to delete file with ID: ${item.id} - ${item.status}`);
100
+ setToastMessage('error', `Failed to delete file with ID: ${item.id}`);
66
101
  }
67
102
  });
68
103
  processing = false;
@@ -99,24 +134,34 @@
99
134
  color: var(--background);
100
135
  }
101
136
  </style>
137
+
102
138
  <main id="file-picker-controls" style="display: flex; justify-content: space-between; gap:1rem;">
103
139
  <div class="file-picker-controls">
104
140
  <div style="display: flex; align-items: last baseline; gap:1rem;">
105
141
  {#each menuItems as item, index (item.id)}
106
142
  <button
107
143
  class={$fileInputStore.activeTab === item.id ? 'active' : ''}
108
- on:click={() => {fileInputStore.update(store => ({ ...store, activeTab: item.id }))}}
144
+ onclick={() => {fileInputStore.update(store => ({ ...store, activeTab: item.id }))}}
109
145
  ><span>{item.label}</span>
110
146
  </button>
111
147
  {/each}
112
- </div>
113
- <div style="display: flex; gap: 2rem; align-items: last baseline;">
114
- {#if $fileInputStore.submissions.length > 0 && $fileInputStore.activeTab === 'cloud'}
115
- <button hidden={!$fileInputStore.manage} title="Delete" on:click={handleDelete} style="color: var(--error-bg);" aria-label="Delete submissions"><i class="fa fa-trash-o"></i></button>
116
- <button hidden={$fileInputStore.manage} title="Select" on:click={handleSelect} style="color: var(--primary-bg);" aria-label="Select submissions"><i class="fa fa-check-circle-o"></i></button>
117
- {/if}
118
- <button title="Cancel" on:click={resetFileInputStore} aria-label="Cancel"><i class="fa fa-times-circle"></i></button>
148
+ </div>
149
+ <div style="display: flex; gap: 1rem; align-items: last baseline;">
150
+ <button hidden={$fileInputStore.manage || disableActions} title="Select" onclick={handleSelect} style="color: var(--primary-bg);" aria-label="Select submissions"><i class="fa fa-check-circle-o"></i></button>
151
+ {#snippet TriggerMenu()}
152
+ <button disabled={disableActions} use:buttonRipple class="w-10" aria-label="Ellipsis" onclick={() => (openActionsMenu = !openActionsMenu)}>
153
+ <i class="fa fa-bars"></i>
154
+ </button>
155
+ {/snippet}
156
+ <DropdownContainer bind:open={openActionsMenu} dropdownTrigger={TriggerMenu}>
157
+ <MenuItem onclick={() => { showProperties = true; openActionsMenu = false; }} icon="fa-info-circle" iconBg="var(--text)" iconSize={iconSize}>Properties</MenuItem>
158
+ <MenuItem onclick={handleDownload} icon="fa-download" iconBg="var(--text)" iconSize={iconSize}>Download</MenuItem>
159
+ <MenuItem onclick={handleDelete} icon="fa-trash-o" iconBg="var(--error-bg)" iconSize={iconSize}>Delete</MenuItem>
160
+ </DropdownContainer>
161
+ <button title="Cancel" onclick={resetFileInputStore} aria-label="Cancel"><i class="fa fa-times-circle"></i></button>
119
162
  </div>
120
163
  </div>
121
164
  </main>
122
- {#if processing}<LinearProgress />{/if}
165
+ {#if processing}<LinearProgress />{/if}
166
+
167
+ <FileProperties bind:showProperties />
@@ -1,20 +1,5 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: Props & {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const Controls: $$__sveltets_2_IsomorphicComponent<{
15
- processing: boolean;
16
- }, {
17
- [evt: string]: CustomEvent<any>;
18
- }, {}, {}, string>;
19
- type Controls = InstanceType<typeof Controls>;
1
+ declare const Controls: import("svelte").Component<{
2
+ processing: any;
3
+ }, {}, "">;
4
+ type Controls = ReturnType<typeof Controls>;
20
5
  export default Controls;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import {Backdrop,Wrapper,fileInputStore, FileInput, isLoggedIn, User, setToastMessage} from '../../../index.js'
2
+ import {Backdrop,Wrapper,fileInputStore, FileInput, User, setToastMessage} from '../../../index.js'
3
3
  import CloudStore from './cloudStore.svelte';
4
4
  import Controls from './controls.svelte';
5
5
 
@@ -14,7 +14,6 @@
14
14
 
15
15
  $fileInputStore.selectedFiles.forEach(file => formData.append('files', file));
16
16
  formData.append('r2_key', $fileInputStore.r2_key);
17
- formData.append('is_loggedin', $isLoggedIn.toString());
18
17
  formData.append('userid', $User.userId);
19
18
 
20
19
  const res = await fetch($fileInputStore.serverUploadUrl, {
@@ -29,9 +28,9 @@
29
28
  return;
30
29
  }
31
30
 
32
- data.forEach((item: { code: number; original_name: string }) => {
31
+ data.forEach((item: { original_name: string; code: number; }) => {
33
32
  if (item.code === 500) {
34
- setToastMessage('error', `Failed to upload file: ${item.original_name} (code: ${item.code})`);
33
+ setToastMessage('error', `Failed to upload file: ${item.original_name}`);
35
34
  }
36
35
  });
37
36
 
@@ -0,0 +1,33 @@
1
+ <script>
2
+ import {Wrapper, Backdrop, fileInputStore} from "../../../index.js";
3
+ let {
4
+ showProperties = $bindable(),
5
+ } = $props();
6
+
7
+ </script>
8
+ <style>
9
+ #file-picker-file-properties h3, span{
10
+ font-size: 14px;
11
+ }
12
+ #file-picker-file-properties span{
13
+ color: var(--text-secondary);
14
+ }
15
+ </style>
16
+ <Backdrop bind:open={showProperties}>
17
+ <Wrapper>
18
+ <div style="display: flex; justify-content: space-between">
19
+ <h3>Properties</h3>
20
+ <button title="Cancel" onclick={() => showProperties = false} aria-label="Cancel"><i class="fa fa-times-circle"></i></button>
21
+ </div>
22
+ <div style="display: flex; flex-direction: column; gap: 1rem; margin-top: 1rem; max-height: 70vh; overflow-y: auto;">
23
+ {#each $fileInputStore.submissions as item (item.id)}
24
+ <div id="file-picker-file-properties">
25
+ <h3>Name: <span>{item.original_name || 'Unnamed file'}</span></h3>
26
+ <h3>Type: <span>{item?.mime_type}</span></h3>
27
+ <h3>Size: <span>{item?.size_bytes}</span> bytes</h3>
28
+ <h3>Created: <span>{item?.created_at ? new Date(item.created_at).toLocaleString() : ''}</span></h3>
29
+ </div>
30
+ {/each}
31
+ </div>
32
+ </Wrapper>
33
+ </Backdrop>
@@ -0,0 +1,11 @@
1
+ export default FileProperties;
2
+ type FileProperties = {
3
+ $on?(type: string, callback: (e: any) => void): () => void;
4
+ $set?(props: Partial<$$ComponentProps>): void;
5
+ };
6
+ declare const FileProperties: import("svelte").Component<{
7
+ showProperties?: any;
8
+ }, {}, "showProperties">;
9
+ type $$ComponentProps = {
10
+ showProperties?: any;
11
+ };
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import {PreviewAudio,PreviewDocument,PreviewImage,PreviewVideo, fileInputStore} from '../../../index.js';
2
+ import {PreviewAudio,PreviewDocument,PreviewImage,PreviewVideo, fileInputStore, PreviewGenericFile} from '../../../index.js';
3
3
 
4
4
  export let media;
5
5
  </script>
@@ -14,6 +14,10 @@
14
14
  <PreviewImage {media} />
15
15
  {:else if $fileInputStore.activeMenu === 'Videos'}
16
16
  <PreviewVideo {media} />
17
+ {:else if $fileInputStore.activeMenu === 'Others'}
18
+ <PreviewGenericFile {media} />
17
19
  {/if}
20
+ {:else}
21
+ <p>No media available.</p>
18
22
  {/if}
19
23
  </div>
@@ -1,7 +1,7 @@
1
1
  <script>
2
2
  import './main.css';
3
3
  import { onMount } from 'svelte';
4
- import {LinearProgress, isLoading, isMobile, Modal, Toast, theme,FilePicker, User, isLoggedIn} from '../../../index.js'
4
+ import {LinearProgress, isLoading, isMobile, Modal, Toast, theme,FilePicker} from '../../../index.js'
5
5
  import Header from './Header/header.svelte';
6
6
  import Menu from './Menu/menu.svelte';
7
7
  import Background from './background.svelte';
@@ -50,18 +50,6 @@
50
50
  localStorage.setItem('theme', 'dark');
51
51
  document.body.setAttribute('data-theme', 'dark');
52
52
  }
53
-
54
- const anonId = localStorage.getItem('anonymous_id');
55
- if(!$isLoggedIn){
56
- if (!anonId) {
57
- const newId = crypto.randomUUID();
58
- localStorage.setItem('anonymous_id', newId);
59
- User.update(user => ({ ...user, userId: newId }));
60
- }else if(anonId) {
61
- User.update(user => ({ ...user, userId: anonId }) );
62
- //console.log("Updated User store with user id:", $User);
63
- }
64
- }
65
53
  }
66
54
  });
67
55
 
package/dist/global.css CHANGED
@@ -173,6 +173,11 @@ button{
173
173
  overflow: hidden;
174
174
  text-overflow: ellipsis;
175
175
  }
176
+ .sierra-text-wrap {
177
+ white-space: normal;
178
+ overflow-wrap: break-word;
179
+ word-break: break-word;
180
+ }
176
181
 
177
182
  .sierra-translate {
178
183
  display: inline-block;
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export { default as PreviewAudio } from './components/Core/others/Previews/Audio
17
17
  export { default as PreviewImage } from './components/Core/others/Previews/Image/image.svelte';
18
18
  export { default as PreviewVideo } from './components/Core/others/Previews/Video/video.svelte';
19
19
  export { default as PreviewDocument } from './components/Core/others/Previews/Document/documents.svelte';
20
+ export { default as PreviewGenericFile } from './components/Core/others/Previews/GenericFile/genericFile.svelte';
20
21
  export { default as Modal } from './components/Core/Alerts/Modal/modal.svelte';
21
22
  export { default as Backdrop } from './components/Core/Alerts/Backdrop/backdrop.svelte';
22
23
  export { default as Wrapper } from './components/Core/Alerts/Wrapper/wrapper.svelte';
@@ -37,7 +38,8 @@ export { User, resetUserStore } from './stores/core/user.js';
37
38
  export { modalStore, resetModalStore } from './stores/core/modal.js';
38
39
  export { editorStore, resetEditorStore } from './stores/modules/editor.js';
39
40
  export { fileInputStore, resetFileInputStore } from './stores/modules/fileInput.js';
41
+ export type { FileInputStoreMediaItem } from './stores/modules/fileInput.js';
40
42
  export { toastCarrier, setToastMessage, clearToastMessage } from './stores/modules/toast.js';
41
- export { getPreviewUrlForMedia, toggleSelectForMedia, removeFileForMedia } from './Hooks/preview.js';
43
+ export { getPreviewUrlForMedia, toggleSelectForMedia, removeFileForMedia, DOCUMENT_MIME_TYPES } from './Hooks/preview.js';
42
44
  export { validateLayoutMenuSections } from './Hooks/layout_menu.js';
43
45
  export { buttonRipple } from './Hooks/button.js';
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ export { default as PreviewAudio } from './components/Core/others/Previews/Audio
22
22
  export { default as PreviewImage } from './components/Core/others/Previews/Image/image.svelte';
23
23
  export { default as PreviewVideo } from './components/Core/others/Previews/Video/video.svelte';
24
24
  export { default as PreviewDocument } from './components/Core/others/Previews/Document/documents.svelte';
25
+ export { default as PreviewGenericFile } from './components/Core/others/Previews/GenericFile/genericFile.svelte';
25
26
  //Alerts
26
27
  export { default as Modal } from './components/Core/Alerts/Modal/modal.svelte';
27
28
  export { default as Backdrop } from './components/Core/Alerts/Backdrop/backdrop.svelte';
@@ -51,6 +52,6 @@ export { editorStore, resetEditorStore } from './stores/modules/editor.js';
51
52
  export { fileInputStore, resetFileInputStore } from './stores/modules/fileInput.js';
52
53
  export { toastCarrier, setToastMessage, clearToastMessage } from './stores/modules/toast.js';
53
54
  //#######################HOOKS/UTILS########################
54
- export { getPreviewUrlForMedia, toggleSelectForMedia, removeFileForMedia } from './Hooks/preview.js';
55
+ export { getPreviewUrlForMedia, toggleSelectForMedia, removeFileForMedia, DOCUMENT_MIME_TYPES } from './Hooks/preview.js';
55
56
  export { validateLayoutMenuSections } from './Hooks/layout_menu.js';
56
57
  export { buttonRipple } from './Hooks/button.js';
@@ -1,16 +1,23 @@
1
+ export type FileInputStoreMediaItem = {
2
+ id: string;
3
+ user_id?: string;
4
+ r2_key: string;
5
+ url: string;
6
+ created_at?: string;
7
+ original_name?: string;
8
+ mime_type?: string;
9
+ size_bytes?: number;
10
+ };
1
11
  export type FileInputState = {
2
12
  uploadModalOpen: boolean;
3
13
  selectedFiles: File[];
4
14
  sizeConstraint: number;
5
- uploadType: Array<'image' | 'audio' | 'video' | 'pdf'>;
15
+ uploadType: Array<'image' | 'audio' | 'video' | 'documents' | 'others'>;
6
16
  activeTab: 'cloud' | 'upload';
7
- activeMenu: 'Documents' | 'Music' | 'Pictures' | 'Videos';
8
- disabledMenuItem: Array<'Music' | 'Documents' | 'Pictures' | 'Videos'> | null;
17
+ activeMenu: 'Documents' | 'Music' | 'Pictures' | 'Videos' | 'Others';
18
+ disabledMenuItem: Array<'Music' | 'Documents' | 'Pictures' | 'Videos' | 'Others'> | null;
9
19
  manage: boolean;
10
- submissions: {
11
- id: string;
12
- url: string;
13
- }[];
20
+ submissions: FileInputStoreMediaItem[];
14
21
  submissionComplete: boolean;
15
22
  r2_key: string;
16
23
  serverGetUrl: string;
@@ -3,10 +3,10 @@ const defaultFileInputState = {
3
3
  uploadModalOpen: false,
4
4
  selectedFiles: [],
5
5
  sizeConstraint: 10 * 1024 * 1024,
6
- uploadType: ['image', 'audio', 'video', 'pdf'],
6
+ uploadType: ['image', 'audio', 'video', 'documents', 'others'],
7
7
  // FilePicker
8
8
  activeTab: 'cloud',
9
- activeMenu: 'Pictures',
9
+ activeMenu: 'Documents',
10
10
  disabledMenuItem: null,
11
11
  manage: false,
12
12
  submissions: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sierra-95/svelte-scaffold",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",