glideshow 0.1.2__py3-none-any.whl
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.
- glideshow/__init__.py +15 -0
- glideshow/__main__.py +3 -0
- glideshow/agent.py +681 -0
- glideshow/build.py +54 -0
- glideshow/cli.py +320 -0
- glideshow/comments.py +35 -0
- glideshow/decks.py +230 -0
- glideshow/docs/agents.md +223 -0
- glideshow/docs/deck-format.md +125 -0
- glideshow/editor/.assetsignore +5 -0
- glideshow/editor/apple-touch-icon.png +0 -0
- glideshow/editor/data/font-metrics.json +1 -0
- glideshow/editor/data/fonts.json +1 -0
- glideshow/editor/data/icons.json +1 -0
- glideshow/editor/data/symbols.js +42 -0
- glideshow/editor/data/themes.js +15 -0
- glideshow/editor/deck/base.css +92 -0
- glideshow/editor/deck/present.js +222 -0
- glideshow/editor/deck/shapes.js +377 -0
- glideshow/editor/deck/template.html +37 -0
- glideshow/editor/editor.css +657 -0
- glideshow/editor/glideshow-1024.png +0 -0
- glideshow/editor/glideshow.svg +10 -0
- glideshow/editor/globals.d.ts +21 -0
- glideshow/editor/home.html +358 -0
- glideshow/editor/index.html +386 -0
- glideshow/editor/js/arrange.js +122 -0
- glideshow/editor/js/boot.js +66 -0
- glideshow/editor/js/boxes.js +25 -0
- glideshow/editor/js/comments.js +85 -0
- glideshow/editor/js/context-menus.js +93 -0
- glideshow/editor/js/dialogs.js +168 -0
- glideshow/editor/js/dock.js +22 -0
- glideshow/editor/js/draw.js +107 -0
- glideshow/editor/js/file.js +318 -0
- glideshow/editor/js/find.js +72 -0
- glideshow/editor/js/format-panel.js +101 -0
- glideshow/editor/js/geometry.js +153 -0
- glideshow/editor/js/guides.js +37 -0
- glideshow/editor/js/insert.js +73 -0
- glideshow/editor/js/keyboard.js +256 -0
- glideshow/editor/js/live.js +48 -0
- glideshow/editor/js/menubar.js +290 -0
- glideshow/editor/js/menus.js +93 -0
- glideshow/editor/js/motion.js +148 -0
- glideshow/editor/js/notes.js +63 -0
- glideshow/editor/js/objects.js +202 -0
- glideshow/editor/js/people.js +73 -0
- glideshow/editor/js/phone-comments.js +43 -0
- glideshow/editor/js/phone-edit.js +253 -0
- glideshow/editor/js/phone-home.js +51 -0
- glideshow/editor/js/phone-mode.js +73 -0
- glideshow/editor/js/phone-sheets.js +153 -0
- glideshow/editor/js/phone-touch.js +145 -0
- glideshow/editor/js/phone-view.js +150 -0
- glideshow/editor/js/pointer.js +407 -0
- glideshow/editor/js/render.js +353 -0
- glideshow/editor/js/shortcuts.js +67 -0
- glideshow/editor/js/slides.js +120 -0
- glideshow/editor/js/spacing.js +23 -0
- glideshow/editor/js/state.js +58 -0
- glideshow/editor/js/storage.js +250 -0
- glideshow/editor/js/text.js +366 -0
- glideshow/editor/js/theme-toggle.js +41 -0
- glideshow/editor/js/theme.js +173 -0
- glideshow/editor/js/toolbar-groups.js +109 -0
- glideshow/editor/js/toolbar.js +372 -0
- glideshow/editor/lib/browser-db.js +47 -0
- glideshow/editor/lib/library.js +612 -0
- glideshow/editor/lib/merge.js +174 -0
- glideshow/editor/lib/move-local.js +51 -0
- glideshow/editor/lib/onefile.js +108 -0
- glideshow/editor/lib/store-browser.js +75 -0
- glideshow/editor/lib/store-folder.js +109 -0
- glideshow/editor/local/sw.js +66 -0
- glideshow/editor/local/tsconfig.json +5 -0
- glideshow/editor/looks/glider.svg +9 -0
- glideshow/editor/looks/thermal-phone.css +144 -0
- glideshow/editor/looks/thermal.css +268 -0
- glideshow/editor/manifest.json +23 -0
- glideshow/editor/phone.css +345 -0
- glideshow/editor/share/share.css +124 -0
- glideshow/editor/share/share.js +256 -0
- glideshow/editor/share/speaker.html +88 -0
- glideshow/editor/tsconfig.json +6 -0
- glideshow/figure.py +33 -0
- glideshow/fonts.py +63 -0
- glideshow/gslides.py +1065 -0
- glideshow/history.py +210 -0
- glideshow/merge.py +210 -0
- glideshow/osutil.py +44 -0
- glideshow/pptx_export.py +207 -0
- glideshow/pptx_import.py +115 -0
- glideshow/publish.py +106 -0
- glideshow/render.py +288 -0
- glideshow/server.py +549 -0
- glideshow/service.py +65 -0
- glideshow/skills/glideshow/SKILL.md +85 -0
- glideshow-0.1.2.dist-info/METADATA +69 -0
- glideshow-0.1.2.dist-info/RECORD +103 -0
- glideshow-0.1.2.dist-info/WHEEL +4 -0
- glideshow-0.1.2.dist-info/entry_points.txt +2 -0
- glideshow-0.1.2.dist-info/licenses/LICENSE +662 -0
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
|
2
|
+
<title>Glideshow</title>
|
|
3
|
+
<rect width="256" height="256" rx="58" fill="#2F6BFF"/>
|
|
4
|
+
<g transform="rotate(-10 128 128)">
|
|
5
|
+
<path d="M36 100 Q128 -4 220 100" fill="none" stroke="#fff" stroke-width="24" stroke-linecap="round"/>
|
|
6
|
+
<g stroke="#fff" stroke-width="10" stroke-linecap="round"><path d="M44 94 L74 142"/><path d="M212 94 L182 142"/></g>
|
|
7
|
+
<rect x="60" y="136" width="136" height="94" rx="28" fill="#fff"/>
|
|
8
|
+
<g fill="#14204A"><circle cx="120" cy="176" r="10.5"/><circle cx="154" cy="176" r="10.5"/></g>
|
|
9
|
+
</g>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// What the editor's classic scripts share at runtime but the type checker can't see (`npm run`-free: `tsc -p editor`):
|
|
2
|
+
// functions deck/shapes.js and the data files put on window, and libraries loaded from a CDN.
|
|
3
|
+
declare var attachConnectors: (root: ParentNode) => void, renderShape: (box: Element) => void, shadeBox: (box: Element) => void, cropBox: (box: Element) => void, fitText: (box: Element) => void;
|
|
4
|
+
declare var shapeAdjust: (...args: any[]) => any, renderMath: (root?: any) => void, unrenderMath: (root?: any) => void, highlightCode: (root?: any) => void;
|
|
5
|
+
declare var LATEX_SYMBOLS: Record<string, string>, CHAR_CATEGORIES: any[];
|
|
6
|
+
declare var Reveal: any, RevealNotes: any, EyeDropper: any;
|
|
7
|
+
interface Window {
|
|
8
|
+
attachConnectors?: typeof attachConnectors; renderShape?: typeof renderShape; highlightCode?: typeof highlightCode; renderMath?: typeof renderMath; unrenderMath?: typeof unrenderMath;
|
|
9
|
+
Reveal?: any; RevealHighlight?: any; renderMathInElement?: any; THEMES?: any[];
|
|
10
|
+
GLIDESHOW_CLOUD?: { user: string; name: string; repo: string } | number; // set by glideshow.org (cloud/worker.js)
|
|
11
|
+
GLIDESHOW?: any; // a shared deck's comments panel (cloud/share.js, editor/share/share.js)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The editor works with HTML elements throughout: querySelector's Element (and an event's target) are treated as one.
|
|
15
|
+
interface Element { style: CSSStyleDeclaration; dataset: DOMStringMap; title: string; innerText: string; offsetWidth: number; isContentEditable: boolean;
|
|
16
|
+
value: any; src: string; href: string; content: string; onclick: any; onchange: any; onpointerenter: any; click(): void; blur(): void; }
|
|
17
|
+
interface EventTarget { closest?(selectors: string): Element | null; tagName?: string; }
|
|
18
|
+
interface Node { closest?(selectors: string): Element | null; }
|
|
19
|
+
interface Document { caretPositionFromPoint?(x: number, y: number): any; matches?(selectors: string): boolean; }
|
|
20
|
+
interface Event { clipboardData?: any; }
|
|
21
|
+
declare var glideMerge: { mergeSlides(base: string[], mine: string[], theirs: string[]): { html: string[], both: number[] }, mergeDecks(base: string, mine: string, theirs: string): { html: string, both: number[] } | null, [k: string]: any };
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<script src="/editor/js/phone-mode.js"></script>
|
|
6
|
+
<title>Glideshow</title>
|
|
7
|
+
<link rel="icon" href="/editor/glideshow.svg" type="image/svg+xml">
|
|
8
|
+
<link rel="apple-touch-icon" href="/editor/apple-touch-icon.png">
|
|
9
|
+
<link rel="manifest" href="/editor/manifest.json"><meta name="apple-mobile-web-app-capable" content="yes"><meta name="mobile-web-app-capable" content="yes"><meta name="apple-mobile-web-app-title" content="Glideshow"> <!-- Add to Home Screen: opens with no browser bars -->
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap">
|
|
12
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..24,400,0..1,0&display=block">
|
|
13
|
+
<link rel="stylesheet" href="/editor/editor.css">
|
|
14
|
+
<link rel="stylesheet" href="/editor/phone.css">
|
|
15
|
+
<script>/* the look of Glideshow's own screens (editor/looks/): Thermal, on a computer and on a phone;
|
|
16
|
+
?look=google gives Google Slides' look and ?look=thermal brings Thermal back, each remembered in this browser */
|
|
17
|
+
{ const q = new URLSearchParams(location.search); let l = 'thermal';
|
|
18
|
+
try { if (q.has('look')) localStorage.setItem('glideshow-look', q.get('look') || 'google'); l = localStorage.getItem('glideshow-look') || l; } catch {}
|
|
19
|
+
if (q.has('look')) l = q.get('look') || 'google';
|
|
20
|
+
if (l !== 'google') document.documentElement.dataset.look = l; }</script>
|
|
21
|
+
<link rel="stylesheet" href="/editor/looks/thermal.css">
|
|
22
|
+
<link rel="stylesheet" href="/editor/looks/thermal-phone.css">
|
|
23
|
+
<style>
|
|
24
|
+
/* The home screen: the library as folders and decks (folders in ~/decks nest), a deck opens in the editor. */
|
|
25
|
+
body { display: block; overflow: auto; background: var(--surface); }
|
|
26
|
+
.hm-bar { position: sticky; top: 0; z-index: 5; display: flex; align-items: center; gap: 16px; height: 64px; padding: 0 16px; background: var(--surface); }
|
|
27
|
+
.hm-bar .brand { text-decoration: none; display: flex; align-items: center; gap: 8px; font-size: 22px; color: var(--ink); min-width: 220px; }
|
|
28
|
+
.hm-bar .brand .logo { width: 36px; height: 36px; display: block; } /* the mascot (glideshow.svg) */
|
|
29
|
+
.hm-bar .brand .stage { padding: 2px 8px; border-radius: 999px; background: var(--blue, #2f6bff); color: #fff; font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; } /* alpha */
|
|
30
|
+
.hm-search { flex: 0 1 720px; display: flex; align-items: center; gap: 10px; height: 48px; padding: 0 16px; border-radius: 24px; background: var(--toolbar); }
|
|
31
|
+
.hm-search input { flex: 1; border: 0; outline: 0; background: none; font: inherit; font-size: 16px; color: var(--ink); }
|
|
32
|
+
.hm-search, .hm-search input { min-width: 0; }
|
|
33
|
+
@media (max-width: 600px) { .hm-bar .brand { min-width: 0; flex: none; } .hm-bar .brand > :not(.logo) { display: none; } } /* a phone (glideshow.org): the search fits beside the logo */
|
|
34
|
+
#hm-app-more { margin-left: auto; }
|
|
35
|
+
#hm-gh { display: inline-grid; place-items: center; }
|
|
36
|
+
.hm-wrap { max-width: 1140px; margin: 0 auto; padding: 0 24px; }
|
|
37
|
+
.hm-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 48px; margin-bottom: 12px; }
|
|
38
|
+
.hm-crumbs { display: flex; align-items: center; flex-wrap: wrap; margin-left: -8px; font-size: 18px; color: var(--ink); }
|
|
39
|
+
.hm-crumbs a { padding: 4px 8px; border-radius: 8px; color: inherit; text-decoration: none; cursor: pointer; }
|
|
40
|
+
.hm-crumbs a:hover, .hm-crumbs a.drop { background: var(--hover); }
|
|
41
|
+
.hm-crumbs a:last-child { font-weight: 500; }
|
|
42
|
+
.hm-crumbs .icon { color: var(--muted); font-size: 20px; }
|
|
43
|
+
.hm-actions { display: flex; align-items: center; gap: 4px; }
|
|
44
|
+
#hm-new { display: flex; align-items: center; gap: 6px; height: 36px; padding: 0 16px 0 12px; border-radius: 18px; margin-right: 8px; }
|
|
45
|
+
.hm-section { margin: 20px 0 10px; font-size: 14px; font-weight: 500; color: var(--muted); }
|
|
46
|
+
.hm-folders, .hm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 20px; } /* the same columns, so folders line up over decks */
|
|
47
|
+
.hm-folders:not(:empty) { margin-bottom: 20px; } /* room before the decks, also when no heading comes between (a folder's time groups) */
|
|
48
|
+
.hm-folder { display: flex; flex-direction: column; gap: 10px; padding: 12px 4px 8px 12px; border-radius: 16px; background: var(--toolbar); cursor: pointer; color: var(--ink); font-size: 14px; transition: background .15s, box-shadow .15s; }
|
|
49
|
+
.hm-folder:hover { background: var(--hover); } .hm-folder.drop { box-shadow: inset 0 0 0 2px var(--blue); }
|
|
50
|
+
.hm-folder .peek { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-right: 8px; } /* three equal slots across the tile */
|
|
51
|
+
.hm-folder .peek .slot { aspect-ratio: 16 / 9; display: grid; place-items: center; overflow: hidden; border-radius: 6px; background: #fff; box-shadow: 0 0 0 1px var(--divider); }
|
|
52
|
+
.hm-folder .peek .slot:empty { background: none; box-shadow: inset 0 0 0 1px var(--divider); opacity: .6; }
|
|
53
|
+
.hm-folder .row { display: flex; align-items: center; gap: 10px; }
|
|
54
|
+
.hm-folder .icon { color: var(--icon); } .hm-folder .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; font-weight: 500; }
|
|
55
|
+
.hm-folder .n { color: var(--muted); font-size: 12px; }
|
|
56
|
+
.hm-folder .more:hover { background: var(--selected); }
|
|
57
|
+
.hm-main { display: block; padding: 12px 24px 48px; } /* editor.css makes <main> the editor's flex row */
|
|
58
|
+
.hm-move { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 12px; margin: 0 0 16px; padding: 12px 16px; border-radius: 12px; background: var(--selected); color: var(--ink); }
|
|
59
|
+
.hm-move span { flex: 1; min-width: 200px; } .hm-move button { font: inherit; font-weight: 500; border: 0; border-radius: 18px; height: 36px; padding: 0 16px; cursor: pointer; } /* its own height: the base button's 28 px left the label low */
|
|
60
|
+
.hm-move-go { background: var(--blue); color: #fff; } .hm-move-not { background: none; color: var(--blue); }
|
|
61
|
+
.hm-card.dragging { opacity: .4; }
|
|
62
|
+
.hm-card { border: 1px solid var(--divider); border-radius: 4px; overflow: hidden; cursor: pointer; background: var(--surface); }
|
|
63
|
+
.hm-card:hover { border-color: var(--blue); }
|
|
64
|
+
.hm-card .pic { position: relative; aspect-ratio: 16 / 9; overflow: hidden; border-bottom: 1px solid var(--divider); background: #fff; }
|
|
65
|
+
.hm-card .pic > div { position: absolute; left: 0; top: 0; transform-origin: 0 0; }
|
|
66
|
+
.hm-card .meta { display: flex; align-items: center; gap: 6px; padding: 10px 4px 10px 12px; }
|
|
67
|
+
.hm-card .meta .txt { flex: 1; min-width: 0; }
|
|
68
|
+
.hm-card .title { font-size: 14px; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
69
|
+
.hm-card .more { width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; }
|
|
70
|
+
.hm-card .more:hover { background: var(--selected); } /* stands out on a hovered row, which is --hover already */
|
|
71
|
+
.hm-pic { position: relative; overflow: hidden; background: #fff; }
|
|
72
|
+
.hm-card .pic .hm-pic { position: absolute; inset: 0; }
|
|
73
|
+
.hm-card .title.derived { font-style: italic; }
|
|
74
|
+
.hm-group { grid-column: 1 / -1;
|
|
75
|
+
margin: 12px 0 -4px; font-size: 14px; font-weight: 500; color: var(--ink); }
|
|
76
|
+
.hm-group:first-child { margin-top: 0; }
|
|
77
|
+
.hm-grid.list .hm-group { margin: 16px 0 4px; padding-left: 12px; }
|
|
78
|
+
.hm-card { transition: box-shadow .15s, border-color .15s; }
|
|
79
|
+
.hm-card:hover { box-shadow: 0 1px 2px rgba(60, 64, 67, .3), 0 1px 3px 1px rgba(60, 64, 67, .15); }
|
|
80
|
+
.hm-preview { position: fixed; z-index: 30; display: flex; flex-direction: column; gap: 8px; padding: 10px; border-radius: 12px; background: var(--surface); box-shadow: var(--menu-shadow); pointer-events: none; }
|
|
81
|
+
.hm-preview > .hm-pic { border-radius: 6px; box-shadow: 0 0 0 1px var(--divider); }
|
|
82
|
+
.hm-preview .strip { display: flex; gap: 8px; } .hm-preview .strip .hm-pic { border-radius: 3px; box-shadow: 0 0 0 1px var(--divider); }
|
|
83
|
+
.hm-preview .cap { font-size: 12px; color: var(--muted); }
|
|
84
|
+
.hm-empty { color: var(--muted); font-size: 14px; }
|
|
85
|
+
.hm-welcome { grid-column: 1 / -1; display: grid; justify-items: start; gap: 12px; max-width: 520px; margin: 24px 0; }
|
|
86
|
+
.hm-welcome h2 { margin: 0; font-size: 24px; font-weight: 400; color: var(--ink); }
|
|
87
|
+
.hm-welcome p { margin: 0; color: var(--muted); font-size: 15px; line-height: 1.5; }
|
|
88
|
+
.hm-welcome button { display: flex; align-items: center; gap: 6px; height: 40px; padding: 0 18px 0 14px; border-radius: 20px; }
|
|
89
|
+
#hm-recent:not([hidden]) ~ #hm-grid { margin-top: 28px; } /* the decks below Recent start clear of its last card */
|
|
90
|
+
.hm-tool { width: 40px; height: 40px; border-radius: 50%; color: var(--icon); }
|
|
91
|
+
|
|
92
|
+
.hm-quit { margin: 120px auto; max-width: 520px; text-align: center; font-size: 16px; color: var(--ink); }
|
|
93
|
+
|
|
94
|
+
.hm-tool:hover { background: var(--hover); } .hm-tool.on { background: var(--selected); }
|
|
95
|
+
/* list view: one row per deck, as Drive's list: a small picture, the title, then folder, opened and slide columns */
|
|
96
|
+
.hm-grid.list { grid-template-columns: 1fr; gap: 0; }
|
|
97
|
+
.hm-grid.list .hm-card { display: flex; align-items: center; gap: 16px; padding: 6px 4px 6px 12px; border: 0; border-bottom: 1px solid var(--divider); border-radius: 0; box-shadow: none; }
|
|
98
|
+
.hm-grid.list .hm-card:hover { background: var(--hover); }
|
|
99
|
+
.hm-grid.list .hm-card .pic { flex: none; width: 64px; height: 36px; border: 1px solid var(--divider); border-radius: 4px; }
|
|
100
|
+
.hm-grid.list .hm-card .pic .hm-pic { transform: scale(calc(64 / 210)); transform-origin: 0 0; }
|
|
101
|
+
.hm-grid.list .hm-card .meta { flex: 1; min-width: 0; padding: 0; }
|
|
102
|
+
.hm-grid.list .hm-card .meta .txt { display: flex; align-items: center; gap: 24px; }
|
|
103
|
+
.hm-grid.list .hm-card .title { flex: 1; }
|
|
104
|
+
.hm-grid.list .hm-card .sub { display: contents; }
|
|
105
|
+
.hm-grid.list .hm-card .sub > span { flex: none; margin: 0; font-size: 13px; color: var(--muted); }
|
|
106
|
+
.hm-grid.list .hm-card .where { width: 160px; } .hm-grid.list .hm-card .when { width: 170px; } .hm-grid.list .hm-card .n { width: 70px; text-align: right; }
|
|
107
|
+
.hm-grid.list .hm-card .where:empty { width: 0; margin-left: -24px; }
|
|
108
|
+
.hm-card .sub { margin-top: 4px; font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
109
|
+
.hm-card .sub > span:empty { display: none; }
|
|
110
|
+
.hm-card .sub > span:not(:empty) ~ span:not(:empty)::before { content: "·"; margin: 0 6px; } /* between the parts that are there */
|
|
111
|
+
.hm-grid.list .hm-card .sub > span::before { content: none !important; }
|
|
112
|
+
|
|
113
|
+
.hm-card .more, .hm-folder .more { flex: none; display: grid; place-items: center; width: 32px; height: 32px; padding: 0; border: 0; border-radius: 50%; background: none; color: var(--icon); }
|
|
114
|
+
.hm-card .more .icon, .hm-folder .more { font-size: 20px; line-height: 1; }
|
|
115
|
+
.hm-card .more .icon { display: block; width: 20px; height: 20px; }
|
|
116
|
+
</style>
|
|
117
|
+
<style id="hm-theme"></style>
|
|
118
|
+
</head>
|
|
119
|
+
<body>
|
|
120
|
+
<header class="hm-bar">
|
|
121
|
+
<a class="brand" href="/" title="Glideshow home"><img class="logo" src="/editor/glideshow.svg" alt=""><span>Glideshow</span><span class="stage" title="Glideshow is in alpha: still changing week to week">alpha</span></a>
|
|
122
|
+
<label class="hm-search"><span class="icon">search</span><input type="search" id="hm-q" placeholder="Search all decks" aria-label="Search decks"></label>
|
|
123
|
+
<div class="hm-end"><a id="hm-gh" class="hm-tool" href="https://github.com/heyodog0/glideshow" target="_blank" rel="noopener" title="Glideshow on GitHub"><svg width="20" height="20" viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg></a><button id="hm-app-more" class="hm-tool icon" title="More">more_vert</button></div>
|
|
124
|
+
</header>
|
|
125
|
+
<main class="hm-wrap hm-main">
|
|
126
|
+
<div class="hm-sky" aria-hidden="true"><p>Slides you own, <em>gliding</em> where you work.</p><img src="/editor/looks/glider.svg" alt=""></div>
|
|
127
|
+
<div class="hm-head"><nav class="hm-crumbs" id="hm-crumbs"></nav><div class="hm-actions">
|
|
128
|
+
<button id="hm-new" class="primary"><span class="icon">add</span>New presentation</button>
|
|
129
|
+
<button id="hm-newfolder" class="hm-tool icon" title="New folder">create_new_folder</button>
|
|
130
|
+
<button id="hm-view" class="hm-tool icon" title="List view">view_list</button>
|
|
131
|
+
<button id="hm-sort" class="hm-tool icon" title="Sort by title">sort_by_alpha</button></div></div>
|
|
132
|
+
<div id="hm-folders-head" class="hm-section">Folders</div><div class="hm-folders" id="hm-folders"></div>
|
|
133
|
+
<div id="hm-recent-head" class="hm-section">Recent</div><div class="hm-grid" id="hm-recent"></div>
|
|
134
|
+
|
|
135
|
+
<div id="hm-decks-head" class="hm-section">Presentations</div><div class="hm-grid" id="hm-grid"></div>
|
|
136
|
+
</main>
|
|
137
|
+
<script src="/editor/js/theme-toggle.js"></script>
|
|
138
|
+
<script>
|
|
139
|
+
const $ = s => document.querySelector(s), div = c => Object.assign(document.createElement('div'), { className: c });
|
|
140
|
+
let decks = [], folders = [], BASE_CSS = '';
|
|
141
|
+
const when = t => { const d = new Date(t * 1000), now = new Date();
|
|
142
|
+
return 'Opened ' + (d.toDateString() === now.toDateString() ? d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) : d.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' })); };
|
|
143
|
+
async function post(url, body) { return (await fetch(url, { method: 'POST', headers: { 'X-Glideshow': '1' }, body: JSON.stringify(body) })).json(); } // the server wants X-Glideshow on a POST
|
|
144
|
+
async function openDeck(d) { const j = await post('/api/open', { path: d.path }); if (!j.error) location.href = '/edit'; }
|
|
145
|
+
/* The folder on show is in the address (#/Lab/2026), so Back and reloads keep it. */
|
|
146
|
+
const here = () => decodeURIComponent(location.hash.replace(/^#\/?/, ''));
|
|
147
|
+
const goTo = f => { location.hash = f ? '/' + f.split('/').map(encodeURIComponent).join('/') : ''; };
|
|
148
|
+
/* A deck's slides, drawn from its own HTML (fetched once per deck; files from /library/<deck id>/…) into a shadow root
|
|
149
|
+
so the deck's CSS stays inside: the card picture is its first slide, the hover preview shows more. */
|
|
150
|
+
const deckDocs = {};
|
|
151
|
+
function deckDoc(d) {
|
|
152
|
+
return deckDocs[d.id] ??= fetch(`/library/${d.id}/deck.html`).then(r => r.text()).catch(() => '').then(html => {
|
|
153
|
+
const doc = new DOMParser().parseFromString(html, 'text/html'), m = (doc.querySelector('.slides')?.dataset.size || '').match(/^(\d+)x(\d+)$/);
|
|
154
|
+
const list = [...doc.querySelectorAll('.slides > section')].flatMap(s => s.querySelector(':scope > section') ? [...s.querySelectorAll(':scope > section')] : [s]);
|
|
155
|
+
return { doc, list, W: m ? +m[1] : 1920, H: m ? +m[2] : 1080 };
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function slidePic(d, info, i, width) {
|
|
159
|
+
const s = info.list[i]?.cloneNode(true); if (!s) return null;
|
|
160
|
+
const base = `/library/${d.id}/`;
|
|
161
|
+
s.querySelectorAll('img[src], video[poster]').forEach(e => { const a = e.tagName === 'IMG' ? 'src' : 'poster', v = e.getAttribute(a); if (!/^(data:|https?:)/.test(v)) e.setAttribute(a, base + v); });
|
|
162
|
+
s.querySelectorAll('iframe, video, aside.notes, script, .box.note').forEach(e => e.remove());
|
|
163
|
+
const host = div('reveal'), slides = div('slides'), box = div('hm-pic');
|
|
164
|
+
box.style.cssText = `width:${width}px;height:${width * info.H / info.W}px`;
|
|
165
|
+
const frame = document.createElement('div'); frame.style.cssText = `width:${info.W}px;height:${info.H}px;transform:scale(${width / info.W});transform-origin:0 0;background:${s.dataset.backgroundColor || '#fff'}`;
|
|
166
|
+
const root = frame.attachShadow({ mode: 'open' });
|
|
167
|
+
root.innerHTML = `<style>${BASE_CSS}\n${info.doc.querySelector('#deck-theme')?.textContent || ''}\n:host { display: block; } .slides > section { display: block !important; position: absolute; left: 0; top: 0; }</style>`;
|
|
168
|
+
slides.append(s); host.append(slides); root.append(host); box.append(frame);
|
|
169
|
+
root.querySelectorAll('.box[data-shape]').forEach(b => window.renderShape?.(b));
|
|
170
|
+
return box;
|
|
171
|
+
}
|
|
172
|
+
const shown = d => d.shown || d.title || d.name;
|
|
173
|
+
const dateOf = d => d.date ? new Date(d.date + 'T12:00') : null; // the deck's own date (the talk's or meeting's)
|
|
174
|
+
const fmtDate = d => dateOf(d)?.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' }) || '';
|
|
175
|
+
const when_ = d => dateOf(d)?.getTime() / 1000 || d.mtime; // what "newest" means in a folder: its date, else last opened // the server names an untitled deck by its first slide's first line
|
|
176
|
+
const cardFit = new ResizeObserver(es => es.forEach(e => { const p = e.target.firstChild; if (p) p.style.zoom = e.target.closest('.list') ? '' : String(e.target.clientWidth / 210); }));
|
|
177
|
+
async function thumb(d, pic, when) { // drawn 210 px wide, then zoomed to fill the card (its column's width)
|
|
178
|
+
const info = await deckDoc(d), p = slidePic(d, info, 0, 210);
|
|
179
|
+
if (p) { pic.append(p); cardFit.observe(pic); }
|
|
180
|
+
if (when) when.textContent = `${info.list.length} slide${info.list.length === 1 ? '' : 's'}`;
|
|
181
|
+
}
|
|
182
|
+
/* Hover a card: a larger look at the deck, its first slide and the next few, as a popover beside the card. */
|
|
183
|
+
let preview = null, previewTimer = 0;
|
|
184
|
+
function hidePreview() { clearTimeout(previewTimer); preview?.remove(); preview = null; }
|
|
185
|
+
let lastPointer = null;
|
|
186
|
+
function showPreview(d, card, e) {
|
|
187
|
+
lastPointer = e;
|
|
188
|
+
hidePreview();
|
|
189
|
+
previewTimer = setTimeout(async () => {
|
|
190
|
+
const info = await deckDoc(d); if (!card.matches(':hover') || !info.list.length) return;
|
|
191
|
+
const p = div('hm-preview'), strip = div('strip'), cap = div('cap');
|
|
192
|
+
p.append(slidePic(d, info, 0, 400));
|
|
193
|
+
info.list.slice(1, 7).forEach((_, i) => strip.append(slidePic(d, info, i + 1, 60)));
|
|
194
|
+
if (strip.children.length) p.append(strip);
|
|
195
|
+
cap.textContent = `${shown(d)}${d.date ? ' · ' + fmtDate(d) : ''} · ${info.list.length} slide${info.list.length === 1 ? '' : 's'}`; p.append(cap);
|
|
196
|
+
document.body.append(p); preview = p;
|
|
197
|
+
const r = card.getBoundingClientRect(), w = p.offsetWidth, h = p.offsetHeight;
|
|
198
|
+
if (card.closest('.list')) { // a row spans the page: beside the pointer, below the row (above it near the bottom)
|
|
199
|
+
const x = lastPointer?.clientX ?? r.left + 120;
|
|
200
|
+
p.style.left = Math.max(8, Math.min(x + 16, innerWidth - w - 8)) + 'px';
|
|
201
|
+
p.style.top = (r.bottom + 6 + h < innerHeight ? r.bottom + 6 : Math.max(72, r.top - 6 - h)) + 'px';
|
|
202
|
+
} else {
|
|
203
|
+
p.style.left = (r.right + 12 + w < innerWidth ? r.right + 12 : Math.max(8, r.left - 12 - w)) + 'px';
|
|
204
|
+
p.style.top = Math.max(72, Math.min(r.top, innerHeight - h - 8)) + 'px';
|
|
205
|
+
}
|
|
206
|
+
}, 450);
|
|
207
|
+
}
|
|
208
|
+
function popMenu(btn, items) {
|
|
209
|
+
document.querySelector('.menu')?.remove();
|
|
210
|
+
const m = div('menu'), r = btn.getBoundingClientRect();
|
|
211
|
+
for (const it of items) {
|
|
212
|
+
if (it === '-') { m.append(div('menu-sep')); continue; }
|
|
213
|
+
const [icon, label, run] = it, b = document.createElement('button'); b.className = 'menu-item'; b.innerHTML = `<span class="menu-icon icon">${icon}</span><span class="menu-label"></span>`;
|
|
214
|
+
b.querySelector('.menu-label').textContent = label; b.onclick = e => { e.stopPropagation(); m.remove(); run(); }; m.append(b);
|
|
215
|
+
}
|
|
216
|
+
document.body.append(m); m.style.left = Math.min(r.left, innerWidth - m.offsetWidth - 8) + 'px'; m.style.top = Math.min(r.bottom + 4, innerHeight - m.offsetHeight - 8) + 'px';
|
|
217
|
+
setTimeout(() => addEventListener('pointerdown', e => { if (!m.contains(e.target)) m.remove(); }, { once: true }));
|
|
218
|
+
}
|
|
219
|
+
const moveItems = d => [['home', 'My decks', () => op(d, 'move', { folder: '' })], ...folders.map(f => ['folder', f.replace(/\//g, ' › '), () => op(d, 'move', { folder: f })])];
|
|
220
|
+
function menu(d, btn) {
|
|
221
|
+
popMenu(btn, [['open_in_new', 'Open', () => openDeck(d)], ['drive_file_move', 'Move to…', () => popMenu(btn, moveItems(d))], ['file_copy', 'Duplicate', () => op(d, 'duplicate')],
|
|
222
|
+
['title', 'Rename', () => rename(d)], ...(CLOUD ? [] : [['folder_open', 'Reveal in Finder', () => op(d, 'reveal')]]), '-',
|
|
223
|
+
['delete', 'Remove', () => confirm(CLOUD?.local ? `Remove "${d.title}" from this browser?` : CLOUD ? `Remove "${d.title}"? (It stays in the repo's history.)` : `Move "${d.title}" to the Trash?`) && op(d, 'delete')]]);
|
|
224
|
+
}
|
|
225
|
+
/* ⋮ in the top bar: Quit server stops the background server (`glideshow`, Glideshow.app or the login agent start it again).
|
|
226
|
+
Online (glideshow.org) there is no server of yours to stop: Sign out instead. */
|
|
227
|
+
const CLOUD = window.GLIDESHOW_CLOUD || null; // online: {user, name, repo}, or {local: true} for a library kept in this browser (editor/local/sw.js)
|
|
228
|
+
// signed in online: decks made in this browser without an account can move into the library (lib/move-local.js)
|
|
229
|
+
const MOVE = '/editor/lib/move-local.js';
|
|
230
|
+
if (CLOUD && !CLOUD.local) import(MOVE).then(m => m.offer(document.querySelector('.hm-main'))).catch(e => console.warn('glideshow: browser decks', e));
|
|
231
|
+
if (CLOUD?.local) { // without an account: say where the decks are, and how to keep them (Safari clears a site's storage after 7 days away)
|
|
232
|
+
const bar = document.createElement('div'), say = document.createElement('span'), go = document.createElement('button');
|
|
233
|
+
bar.className = 'hm-move'; say.textContent = 'These decks live only in this browser, which can clear them. To keep them, download each one (File → Download) or sign in.';
|
|
234
|
+
go.className = 'hm-move-go primary'; go.textContent = 'Sign in'; go.onclick = () => { location.href = '/login'; };
|
|
235
|
+
const f = CLOUD.folder; // a folder on this computer (editor/lib/store-folder.js)
|
|
236
|
+
if (f?.ok) say.textContent = `These decks are in the folder “${f.name}” on this computer, and only there: not synced, and they can't be shared. Sign in to keep them in Drive or GitHub.`;
|
|
237
|
+
else if (f) { // the browser forgot its permission for the folder (a new session): ask again, with this click
|
|
238
|
+
say.textContent = `Glideshow needs your OK to open the folder “${f.name}” again. Until then, changes stay in this browser.`;
|
|
239
|
+
go.textContent = `Open “${f.name}”`;
|
|
240
|
+
go.onclick = async () => {
|
|
241
|
+
const m = await import('/editor/lib/store-folder.js'), dir = await m.getFolder();
|
|
242
|
+
if (dir && await m.folderAllowed(dir, true)) { navigator.serviceWorker.controller?.postMessage({ type: 'library-changed' }); setTimeout(() => location.reload(), 400); }
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
bar.append(say, go); document.querySelector('.hm-main')?.prepend(bar);
|
|
246
|
+
}
|
|
247
|
+
$('#hm-app-more').onclick = e => { e.stopPropagation(); popMenu(e.currentTarget, [...(CLOUD?.local ? [CLOUD.folder ? ['folder', `Your decks are in the folder “${CLOUD.folder.name}”`, () => {}] : ['devices', 'Your decks are in this browser', () => {}],
|
|
248
|
+
...(CLOUD.folder ? [['folder_off', 'Stop using this folder (the decks stay in it, and in this browser)', async () => {
|
|
249
|
+
await (await import('/editor/lib/store-folder.js')).setFolder(null); navigator.serviceWorker.controller?.postMessage({ type: 'library-changed' }); setTimeout(() => location.reload(), 400); }]] : []),
|
|
250
|
+
['login', 'Sign in to keep decks in Drive or GitHub, and share them', () => { location.href = '/login'; }]] : CLOUD ? [['folder_shared', `Library: ${CLOUD.repo || ''}`, () => { location.href = '/setup'; }], ['logout', `Sign out (${CLOUD.user || ''})`, () => { location.href = '/logout'; }]] : [['power_settings_new', 'Quit server', async () => {
|
|
251
|
+
if (!confirm('Stop the Glideshow server? Open Glideshow again (or run `glideshow`) to restart it.')) return;
|
|
252
|
+
await post('/api/quit', {}).catch(() => {});
|
|
253
|
+
document.body.innerHTML = '<p class="hm-quit">The Glideshow server has stopped. Open Glideshow again, or run <code>glideshow</code>, to restart it.</p>';
|
|
254
|
+
}]]), ...(document.documentElement.classList.contains('phone') ? ['-', ['code', 'Glideshow on GitHub', () => open('https://github.com/heyodog0/glideshow', '_blank', 'noopener')]] : [])]); }; // (a phone has no GitHub icon in the bar)
|
|
255
|
+
async function op(d, what, extra = {}) { const j = await post('/api/deckop', { path: d.path, op: what, ...extra }); if (j.error) alert(j.error); else load(); }
|
|
256
|
+
async function folderOp(what, path, name) { const j = await post('/api/folder', { op: what, path, name }); if (j.error) alert(j.error); return !j.error; }
|
|
257
|
+
function rename(d) { const t = prompt('Rename', d.title); if (t && t.trim()) op(d, 'rename', { title: t.trim() }); }
|
|
258
|
+
const pref = (k, d) => { try { return localStorage.getItem('slides-home-' + k) || d; } catch { return d; } };
|
|
259
|
+
const setPref = (k, v) => { try { localStorage.setItem('slides-home-' + k, v); } catch {} };
|
|
260
|
+
/* Drag a card onto a folder (or a breadcrumb) to move the deck there. */
|
|
261
|
+
let dragged = null;
|
|
262
|
+
function dropTarget(el, folder) {
|
|
263
|
+
el.ondragover = e => { if (!dragged) return; e.preventDefault(); el.classList.add('drop'); };
|
|
264
|
+
el.ondragleave = () => el.classList.remove('drop');
|
|
265
|
+
el.ondrop = e => { e.preventDefault(); el.classList.remove('drop'); if (dragged && dragged.folder !== folder) op(dragged, 'move', { folder }); };
|
|
266
|
+
}
|
|
267
|
+
function crumbs(cur) {
|
|
268
|
+
const parts = cur ? cur.split('/') : [], out = [];
|
|
269
|
+
[['My decks', ''], ...parts.map((p, i) => [p, parts.slice(0, i + 1).join('/')])].forEach(([label, f], i) => {
|
|
270
|
+
if (i) out.push(Object.assign(document.createElement('span'), { className: 'icon', textContent: 'chevron_right' }));
|
|
271
|
+
const a = document.createElement('a'); a.textContent = label; a.onclick = () => goTo(f); dropTarget(a, f); out.push(a);
|
|
272
|
+
});
|
|
273
|
+
$('#hm-crumbs').replaceChildren(...out);
|
|
274
|
+
}
|
|
275
|
+
const PEEK_W = 240; // folder-tile pictures are drawn this wide, then zoomed to their slot
|
|
276
|
+
function folderTile(f) {
|
|
277
|
+
const mine = decks.filter(d => d.folder === f || d.folder?.startsWith(f + '/')).sort((a, b) => when_(b) - when_(a)), inside = mine.length;
|
|
278
|
+
const t = div('hm-folder'), peek = div('peek'); // the first slides of its three newest decks, each filling a third of the tile
|
|
279
|
+
const slots = [0, 1, 2].map(() => div('slot')); peek.append(...slots);
|
|
280
|
+
const fit = () => slots.forEach(s => { const p = s.firstChild; if (p) p.style.zoom = s.clientWidth / PEEK_W; });
|
|
281
|
+
mine.slice(0, 3).forEach(async (d, i) => { const p = slidePic(d, await deckDoc(d), 0, PEEK_W); if (p) { slots[i].append(p); fit(); } });
|
|
282
|
+
new ResizeObserver(fit).observe(peek);
|
|
283
|
+
t.innerHTML = '<div class="row"><span class="icon">folder</span><span class="name"></span><span class="n"></span><button class="more icon" title="More actions">more_vert</button></div>';
|
|
284
|
+
t.prepend(peek);
|
|
285
|
+
t.querySelector('.name').textContent = f.split('/').pop(); t.querySelector('.n').textContent = inside ? `${inside} deck${inside === 1 ? '' : 's'}` : 'Empty';
|
|
286
|
+
t.onclick = () => goTo(f); dropTarget(t, f);
|
|
287
|
+
t.querySelector('.more').onclick = e => { e.stopPropagation(); popMenu(e.currentTarget, [
|
|
288
|
+
['drive_file_rename_outline', 'Rename', async () => { const n = prompt('Rename folder', f.split('/').pop()); if (n && n.trim() && await folderOp('rename', f, n.trim())) load(); }],
|
|
289
|
+
['delete', 'Remove', async () => { if (await folderOp('delete', f)) load(); }]]); };
|
|
290
|
+
return t;
|
|
291
|
+
}
|
|
292
|
+
function card(d, q) {
|
|
293
|
+
const c = div('hm-card'), pic = div('pic'), meta = div('meta');
|
|
294
|
+
meta.innerHTML = `<div class="txt"><div class="title"></div><div class="sub"><span class="where"></span><span class="when"></span><span class="n"></span></div></div><button class="more" title="More actions"><span class="icon">${PHONE ? 'more_horiz' : 'more_vert'}</span></button>`;
|
|
295
|
+
const t = meta.querySelector('.title'); t.textContent = shown(d); t.classList.toggle('derived', !!d.shown && d.shown !== d.title);
|
|
296
|
+
meta.querySelector('.where').textContent = q && d.folder ? d.folder.replace(/\//g, ' › ') : '';
|
|
297
|
+
meta.querySelector('.when').textContent = fmtDate(d) || when(d.mtime); c.title = `${d.path}\n${when(d.mtime)}`;
|
|
298
|
+
meta.querySelector('.more').onclick = e => { e.stopPropagation(); hidePreview(); menu(d, e.currentTarget); };
|
|
299
|
+
c.draggable = true; c.ondragstart = e => { dragged = d; hidePreview(); c.classList.add('dragging'); e.dataTransfer.setData('text/plain', shown(d)); };
|
|
300
|
+
c.ondragend = () => { dragged = null; c.classList.remove('dragging'); };
|
|
301
|
+
c.onmouseenter = e => showPreview(d, c, e); c.onmousemove = e => { lastPointer = e; }; c.onmouseleave = hidePreview; c.onpointerdown = hidePreview;
|
|
302
|
+
c.onclick = () => openDeck(d); c.append(pic, meta); thumb(d, pic, meta.querySelector('.n')); return c;
|
|
303
|
+
}
|
|
304
|
+
function show() {
|
|
305
|
+
const q = $('#hm-q').value.trim().toLowerCase(), byTitle = pref('sort', 'time') === 'title';
|
|
306
|
+
let cur = here(); if (cur && !folders.includes(cur)) cur = '';
|
|
307
|
+
crumbs(cur);
|
|
308
|
+
// a search looks through the whole library; otherwise this folder (decks outside the library show at the top).
|
|
309
|
+
// On a phone the top level is every deck by when it was last opened, as the Slides app (folders: the folder button).
|
|
310
|
+
const all = PHONE && !cur && !q;
|
|
311
|
+
const list = decks.filter(d => q ? [d.title, d.name, d.shown || ''].some(t => t.toLowerCase().includes(q)) : all || (d.folder ?? '') === cur)
|
|
312
|
+
.sort((a, b) => byTitle ? shown(a).localeCompare(shown(b)) : PHONE ? b.mtime - a.mtime : when_(b) - when_(a));
|
|
313
|
+
const subs = q || all ? [] : folders.filter(f => cur ? f.startsWith(cur + '/') && !f.slice(cur.length + 1).includes('/') : !f.includes('/')).sort((a, b) => a.localeCompare(b));
|
|
314
|
+
const asList = pref('view', 'grid') === 'list';
|
|
315
|
+
$('#hm-grid').classList.toggle('list', asList);
|
|
316
|
+
$('#hm-view').textContent = asList ? 'grid_view' : 'view_list'; $('#hm-view').title = asList ? 'Grid view' : 'List view';
|
|
317
|
+
$('#hm-sort').classList.toggle('on', byTitle); $('#hm-sort').title = byTitle ? 'Sort by last opened' : 'Sort by title';
|
|
318
|
+
// Recent only once the top level doesn't already show every deck (some are in folders, or there are many): a new
|
|
319
|
+
// library would otherwise show each deck twice
|
|
320
|
+
const recent = !cur && !q && !PHONE && (decks.some(d => d.folder) || decks.length > 8) ? [...decks].sort((a, b) => b.mtime - a.mtime).slice(0, 8) : [];
|
|
321
|
+
$('#hm-recent').replaceChildren(...recent.map(d => card(d, true))); $('#hm-recent').hidden = $('#hm-recent-head').hidden = !recent.length;
|
|
322
|
+
$('#hm-recent').classList.toggle('list', asList);
|
|
323
|
+
$('#hm-folders').replaceChildren(...subs.map(folderTile)); $('#hm-folders-head').hidden = !subs.length;
|
|
324
|
+
const group = d => { const t = dateOf(d); return t ? t.toLocaleDateString([], { month: 'long', year: 'numeric' }) : 'No date'; };
|
|
325
|
+
const items = [];
|
|
326
|
+
list.forEach((d, i) => { if (!byTitle && !q && !PHONE && (i === 0 || group(d) !== group(list[i - 1]))) items.push(Object.assign(div('hm-group'), { textContent: group(d) })); items.push(card(d, q)); });
|
|
327
|
+
const empty = q || !(subs.length || $('#hm-recent').children.length) ? [Object.assign(div('hm-empty'), { textContent: q ? 'No decks match.' : PHONE ? 'No presentations here yet. Start one with +.' : 'No presentations here yet. Start one with New presentation.' })] : [];
|
|
328
|
+
$('#hm-grid').replaceChildren(...(list.length ? items : !decks.length && !folders.length && !q && !cur ? [welcome()] : empty));
|
|
329
|
+
|
|
330
|
+
$('#hm-decks-head').hidden = !subs.length || !byTitle && !q; // the time groups head the decks themselves
|
|
331
|
+
}
|
|
332
|
+
/* A library with nothing in it yet: what Glideshow is, where the decks will be, and the one thing to do first. */
|
|
333
|
+
let libraryHome = '~/decks';
|
|
334
|
+
function welcome() {
|
|
335
|
+
const w = div('hm-welcome'), where = CLOUD?.local ? 'in this browser' : CLOUD ? `in ${CLOUD.repo}` : `in ${libraryHome} on this computer`;
|
|
336
|
+
w.innerHTML = `<h2>Welcome to Glideshow</h2><p></p><button class="primary"><span class="icon">add</span>New presentation</button>`;
|
|
337
|
+
w.querySelector('p').textContent = `Your decks will be kept ${where}. Each one is a folder with a deck.html and its pictures, which you, Claude or any other agent can open and edit.`;
|
|
338
|
+
w.querySelector('button').onclick = () => $('#hm-new').click();
|
|
339
|
+
return w;
|
|
340
|
+
}
|
|
341
|
+
async function load() {
|
|
342
|
+
if (!CLOUD) fetch('/api/library').then(r => r.json()).then(l => { if (l?.home && l.home !== libraryHome) { libraryHome = l.home; if (!decks.length) show(); } }).catch(() => {});
|
|
343
|
+
const [d, f] = await Promise.all([fetch('/api/decks').then(r => r.json()).catch(e => ({ error: String(e) })), fetch('/api/folders').then(r => r.json()).catch(() => [])]);
|
|
344
|
+
if (!Array.isArray(d)) { document.querySelector('#hm-grid').replaceChildren(Object.assign(div('hm-empty'), { textContent: `Couldn't load your decks: ${d?.error || 'no answer'}. Reload to try again.` })); return; }
|
|
345
|
+
decks = d; folders = Array.isArray(f) ? f : []; show();
|
|
346
|
+
}
|
|
347
|
+
$('#hm-q').oninput = show;
|
|
348
|
+
addEventListener('hashchange', show);
|
|
349
|
+
$('#hm-view').onclick = () => { setPref('view', pref('view', 'grid') === 'list' ? 'grid' : 'list'); show(); };
|
|
350
|
+
$('#hm-sort').onclick = () => { setPref('sort', pref('sort', 'time') === 'title' ? 'time' : 'title'); show(); };
|
|
351
|
+
$('#hm-new').onclick = async () => { const j = await post('/api/open', { new: true, folder: here() }); if (!j.error) location.href = '/edit'; };
|
|
352
|
+
$('#hm-newfolder').onclick = async () => { const n = prompt('New folder'); if (n && n.trim() && await folderOp('create', here(), n.trim())) { await load(); } };
|
|
353
|
+
(async () => { BASE_CSS = await fetch('/editor/deck/base.css').then(r => r.text()); await load(); })();
|
|
354
|
+
</script>
|
|
355
|
+
<script src="/editor/deck/shapes.js"></script>
|
|
356
|
+
<script src="/editor/js/phone-home.js"></script>
|
|
357
|
+
</body>
|
|
358
|
+
</html>
|