@axium/storage 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { StorageItemMetadata } from '../common.js';
3
+ export declare const previews: Map<string, Snippet<[item: StorageItemMetadata<Record<string, unknown>>]>>;
4
+ export interface Opener {
5
+ /** Mime types supported by this opener */
6
+ types: string[];
7
+ name: string;
8
+ /** Get a URL to open the item with another plugin */
9
+ openURL(item: StorageItemMetadata): string;
10
+ }
11
+ export declare const openers: Opener[];
@@ -0,0 +1,2 @@
1
+ export const previews = new Map();
2
+ export const openers = [];
package/lib/List.svelte CHANGED
@@ -1,11 +1,11 @@
1
1
  <script lang="ts">
2
- import { AccessControlDialog, FormDialog, Icon } from '@axium/client/components';
2
+ import { AccessControlDialog, FormDialog, Icon, Popover } from '@axium/client/components';
3
3
  import '@axium/client/styles/list';
4
4
  import type { AccessControllable, UserPublic } from '@axium/core';
5
5
  import { formatBytes } from '@axium/core/format';
6
6
  import { forMime as iconForMime } from '@axium/core/icons';
7
7
  import { downloadItem, getDirectoryMetadata, updateItemMetadata } from '@axium/storage/client';
8
- import previews from '@axium/storage/client/previews';
8
+ import { openers, previews } from '@axium/storage/client/3rd-party';
9
9
  import type { StorageItemMetadata } from '@axium/storage/common';
10
10
 
11
11
  let {
@@ -22,7 +22,7 @@
22
22
 
23
23
  {#snippet action(name: string, icon: string, i: number, preview: boolean = false)}
24
24
  <span
25
- class={[!preview && 'action']}
25
+ class={['icon-text', !preview ? 'action' : 'preview-action']}
26
26
  onclick={() => {
27
27
  activeIndex = i;
28
28
  dialogs[name].showModal();
@@ -48,6 +48,7 @@
48
48
  <span>Size</span>
49
49
  </div>
50
50
  {#each items as item, i (item.id)}
51
+ {@const itemOpeners = openers.filter(opener => opener.types.includes(item.type))}
51
52
  <div
52
53
  class="list-item"
53
54
  onclick={async () => {
@@ -71,7 +72,7 @@
71
72
  {@render action('rename', 'pencil', i)}
72
73
  {@render action('share' + item.id, 'user-group', i)}
73
74
  <AccessControlDialog
74
- bind:dialog={dialogs['share' + item.id]}
75
+ bind:dialog={dialogs['share:' + item.id]}
75
76
  {item}
76
77
  itemType="storage"
77
78
  editable={(item.acl?.find(
@@ -85,13 +86,33 @@
85
86
  {@render action('download', 'download', i)}
86
87
  {@render action('trash', 'trash', i)}
87
88
  <dialog bind:this={dialogs['preview:' + item.id]} class="preview">
88
- <div class="title">{item.name}</div>
89
- <div class="actions">
90
- {@render action('rename', 'pencil', i, true)}
91
- {@render action('share' + item.id, 'user-group', i, true)}
92
- {@render action('download', 'download', i, true)}
93
- {@render action('trash', 'trash', i, true)}
94
- <span onclick={() => dialogs['preview:' + item.id].close()}><Icon i="xmark" --size="20px" /></span>
89
+ <div class="preview-top-bar">
90
+ <div class="title">{item.name}</div>
91
+ {#if itemOpeners.length}
92
+ {@const [first, ...others] = itemOpeners}
93
+ <div class="openers">
94
+ <span>Open with <a href={first.openURL(item)} target="_blank">{first.name}</a></span>
95
+ {#if others.length}
96
+ <Popover>
97
+ {#snippet toggle()}
98
+ <span class="popover-toggle"><Icon i="caret-down" /></span>
99
+ {/snippet}
100
+ {#each others as opener}
101
+ <a href={opener.openURL(item)} target="_blank">{opener.name}</a>
102
+ {/each}
103
+ </Popover>
104
+ {/if}
105
+ </div>
106
+ {/if}
107
+ <div class="actions">
108
+ {@render action('rename', 'pencil', i, true)}
109
+ {@render action('share:' + item.id, 'user-group', i, true)}
110
+ {@render action('download', 'download', i, true)}
111
+ {@render action('trash', 'trash', i, true)}
112
+ <span class="mobile-hide" onclick={() => dialogs['preview:' + item.id].close()}>
113
+ <Icon i="xmark" --size="20px" />
114
+ </span>
115
+ </div>
95
116
  </div>
96
117
  <div class="content">
97
118
  {#if item.type.startsWith('image/')}
@@ -108,12 +129,13 @@
108
129
  <p>PDF not displayed? <a href={item.dataURL} download={item.name}>Download</a></p>
109
130
  </object>
110
131
  {:else if item.type.startsWith('text/')}
111
- <pre class="preview-text">{#await downloadItem(item.id).then(b => b.text()) then content}{content}{/await}</pre>
132
+ <pre
133
+ class="full-fill preview-text">{#await downloadItem(item.id).then( b => b.text() ) then content}{content}{/await}</pre>
112
134
  {:else if previews.has(item.type)}
113
135
  {@render previews.get(item.type)!(item)}
114
136
  {:else}
115
- <div class="no-preview">
116
- <Icon i="eye-slash" />
137
+ <div class="full-fill no-preview">
138
+ <Icon i="eye-slash" --size="50px" />
117
139
  <span>Preview not available</span>
118
140
  </div>
119
141
  {/if}
@@ -175,28 +197,45 @@
175
197
  inset: 0;
176
198
  width: 100%;
177
199
  height: 100%;
178
- background-color: #0008;
200
+ background-color: #000a;
179
201
  border: none;
180
202
  padding: 1em;
181
203
  word-wrap: normal;
204
+ anchor-scope: --preview-openers;
182
205
 
183
- .title,
184
- .actions {
185
- align-items: center;
206
+ .preview-action:hover {
207
+ cursor: pointer;
208
+ }
209
+
210
+ .preview-top-bar {
186
211
  display: flex;
187
212
  align-items: center;
188
213
  gap: 1em;
214
+ justify-content: space-between;
215
+ padding: 0;
189
216
  position: absolute;
190
- top: 1em;
217
+ inset: 0.5em 1em 0;
218
+ height: fit-content;
219
+
220
+ > div {
221
+ display: flex;
222
+ gap: 1em;
223
+ align-items: center;
224
+ }
225
+ }
226
+
227
+ .openers {
191
228
  padding: 1em;
192
- height: 1em;
229
+ border: 1px solid var(--border-accent);
230
+ border-radius: 1em;
231
+ height: 2em;
232
+ anchor-name: --preview-openers;
193
233
  }
194
234
 
195
- .title {
196
- left: 1em;
197
- overflow: hidden;
198
- white-space: nowrap;
199
- text-overflow: ellipsis;
235
+ .openers :global([popover]) {
236
+ inset: anchor(bottom) anchor(right) auto anchor(left);
237
+ position-anchor: --preview-openers;
238
+ width: anchor-size(width);
200
239
  }
201
240
 
202
241
  .actions {
@@ -207,11 +246,14 @@
207
246
  position: absolute;
208
247
  inset: 3em 10em 0;
209
248
 
210
- .preview-text {
249
+ .full-fill {
211
250
  position: absolute;
212
251
  inset: 0;
213
252
  width: 100%;
214
253
  height: 100%;
254
+ }
255
+
256
+ .preview-text {
215
257
  white-space: pre-wrap;
216
258
  overflow-y: scroll;
217
259
  line-height: 1.6;
@@ -229,13 +271,27 @@
229
271
  }
230
272
 
231
273
  @media (width < 700px) {
232
- .actions {
233
- top: 3em;
234
- left: 1em;
274
+ .preview-top-bar {
275
+ flex-direction: column;
276
+
277
+ .actions {
278
+ justify-content: space-around;
279
+ width: 100%;
280
+
281
+ .preview-action {
282
+ padding: 1em;
283
+ flex: 1 1 0;
284
+ border-radius: 1em;
285
+ border: 1px solid var(--border-accent);
286
+ padding: 1em;
287
+ justify-content: center;
288
+ display: flex;
289
+ }
290
+ }
235
291
  }
236
292
 
237
293
  .content {
238
- inset: 5em 1em 0;
294
+ inset: 10em 1em 0;
239
295
  }
240
296
  }
241
297
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axium/storage",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "author": "James Prevett <axium@jamespre.dev>",
5
5
  "description": "User file storage for Axium",
6
6
  "funding": {
@@ -1,4 +0,0 @@
1
- import type { Snippet } from 'svelte';
2
- import type { StorageItemMetadata } from '../common.js';
3
- declare const _default: Map<string, Snippet<[item: StorageItemMetadata<Record<string, unknown>>]>>;
4
- export default _default;
@@ -1 +0,0 @@
1
- export default new Map();