Skip to content

Commit

Permalink
Implement permalinking with GitHub gists
Browse files Browse the repository at this point in the history
  • Loading branch information
Amphiluke committed Dec 10, 2023
1 parent ae2ace2 commit 7aa9063
Show file tree
Hide file tree
Showing 11 changed files with 175 additions and 132 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "lindsvg-pwa",
"private": true,
"version": "2.2.2",
"version": "2.3.0",
"type": "module",
"scripts": {
"lint": "eslint \"src/**/*.{mjs,vue}\"",
Expand Down
4 changes: 2 additions & 2 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script setup>
import {onMounted} from "vue";
import {applyLaunchParams} from "./pwaCtrl.mjs";
import {processLaunchOptions} from "./launchCtrl.mjs";
import PlotArea from "./components/PlotArea.vue";
import TheSidebar from "./components/TheSidebar.vue";
import ThePopover from "./components/ThePopover.vue";
onMounted(() => applyLaunchParams());
onMounted(() => processLaunchOptions());
</script>

<template>
Expand Down
2 changes: 1 addition & 1 deletion src/assets/icons.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/PanelAbout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ let appVersion = __PACKAGE_VERSION__;
There are a few L-system collections built in the app for demonstration purposes. These L-systems were gathered from various sources including the web, books, and articles. I appreciate the authors of these L-systems (you may find their names <a href="https://github.com/Amphiluke/lindsvg/blob/pwa/src/stores/bank.mjs" target="_blank" rel="noopener">in the app sources</a>).
</p>
<h3>Links</h3>
<ul>
<ul :class="interfaceStyles.list">
<li>
<a href="https://codepen.io/collection/DVzqWb" target="_blank" rel="noopener">Advanced examples</a>
</li>
Expand Down
35 changes: 21 additions & 14 deletions src/components/PanelCollections.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup>
import {ref, computed, onMounted} from "vue";
import {ref, computed} from "vue";
import {refDebounced} from "@vueuse/core";
import {useCollectionsStore, isUserDefined, USER_DEFINED_COLLECTION_ID} from "../stores/collections.mjs";
import {useLSystemStore} from "../stores/lSystem.mjs";
Expand Down Expand Up @@ -52,14 +52,12 @@ function deleteLSystem(lid) {
collectionsStore.deleteLSystem(lid);
}
onMounted(() => {
let searchParams = new URLSearchParams(location.search);
let cid = searchParams.get("cid");
let lid = searchParams.get("lid");
if (cid && lid) {
plot(cid, lid);
}
});
function copyPermalink(cid, lid) {
let url = new URL(location.origin + location.pathname);
url.searchParams.set("cid", cid);
url.searchParams.set("lid", lid);
navigator.clipboard.writeText(url.toString());
}
</script>

<template>
Expand Down Expand Up @@ -117,6 +115,14 @@ onMounted(() => {
title="Delete this L-system"
@click="deleteLSystem(lid)"
/>
<button
v-else
type="button"
tabindex="-1"
:class="[$style.permalinkButton, interfaceStyles.iconButton, interfaceStyles.iconButtonLink]"
title="Copy L-system permalink"
@click="copyPermalink(cid, lid)"
/>
<button
type="button"
:class="[$style.exploreButton, interfaceStyles.iconButton, interfaceStyles.iconButtonConfig]"
Expand Down Expand Up @@ -207,18 +213,19 @@ onMounted(() => {
.collectionItemName {
cursor: default;
flex-grow: 1;
overflow: hidden;
padding: 5px 10px;
text-overflow: ellipsis;
white-space: nowrap;
}
.permalinkButton,
.exploreButton,
.deleteLSystemButton {
flex-shrink: 0;
}
.collectionItems li:not(:hover, .active) {
.exploreButton:not(:focus),
.deleteLSystemButton:not(:focus) {
opacity: 0.01;
}
.collectionItems li:not(:hover, .active) :where(.permalinkButton, .exploreButton, .deleteLSystemButton):not(:focus) {
opacity: 0.01;
}
</style>
124 changes: 50 additions & 74 deletions src/components/PanelSharing.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup>
import {computed, useCssModule} from "vue";
import {useCollectionsStore, isUserDefined} from "../stores/collections.mjs";
import {computed} from "vue";
import {useCollectionsStore} from "../stores/collections.mjs";
import {useLSystemStore} from "../stores/lSystem.mjs";
import {useInterfaceStore} from "../stores/interface.mjs";
import {useObjectUrl, useFileDialog, useShare} from "@vueuse/core";
Expand All @@ -10,7 +10,6 @@ import panelStyles from "../styles/panel.module.css";
let collectionsStore = useCollectionsStore();
let lSystemStore = useLSystemStore();
let interfaceStore = useInterfaceStore();
let cssModule = useCssModule();
let fileDialog = useFileDialog({accept: ".lsvg", multiple: false, reset: true});
fileDialog.onChange(async ([file]) => {
Expand All @@ -27,19 +26,17 @@ fileDialog.onChange(async ([file]) => {
let svgBlob = computed(() => new Blob([lSystemStore.svgCode], {type: "image/svg+xml"}));
let svgURL = useObjectUrl(svgBlob);
let lsvgBlob = computed(() => {
let lsvg = JSON.stringify({
_version: __PACKAGE_VERSION__,
axiom: lSystemStore.axiom,
alpha: lSystemStore.alpha,
theta: lSystemStore.theta,
step: lSystemStore.step,
iterations: lSystemStore.iterations,
rules: lSystemStore.rules,
attributes: lSystemStore.attributes,
});
return new Blob([lsvg], {type: "application/json"});
});
let lsvg = computed(() => ({
_version: __PACKAGE_VERSION__,
axiom: lSystemStore.axiom,
alpha: lSystemStore.alpha,
theta: lSystemStore.theta,
step: lSystemStore.step,
iterations: lSystemStore.iterations,
rules: lSystemStore.rules,
attributes: lSystemStore.attributes,
}));
let lsvgBlob = computed(() => new Blob([JSON.stringify(lsvg.value)], {type: "application/json"}));
let lsvgURL = useObjectUrl(lsvgBlob);
let {share, isSupported: isShareSupported} = useShare();
Expand All @@ -51,21 +48,9 @@ function launchShare() {
});
}
let permalink = computed(() => {
if (!collectionsStore.selectedCID || !collectionsStore.selectedLID || isUserDefined(collectionsStore.selectedCID)) {
return "";
}
let {location} = window;
let url = new URL(location.origin + location.pathname);
url.searchParams.set("cid", collectionsStore.selectedCID);
url.searchParams.set("lid", collectionsStore.selectedLID);
return url.toString();
});
async function copyPermalink({target}) {
await navigator.clipboard.writeText(permalink.value);
target.classList.add(cssModule.copySuccess);
setTimeout(() => target.classList.remove(cssModule.copySuccess), 4000);
function copyLSVG() {
let text = JSON.stringify(lsvg.value, null, 2);
navigator.clipboard.writeText(text);
}
</script>

Expand Down Expand Up @@ -119,26 +104,33 @@ async function copyPermalink({target}) {

<hr>

<h3>Permalink for the selected L-system</h3>
<textarea
:class="$style.permalinkField"
:value="permalink"
readonly
@click="$event.target.select()"
/>
<button
type="button"
:class="[interfaceStyles.button, $style.permalinkCopyButton]"
:disabled="!permalink"
@click="copyPermalink"
>
<span :class="$style.permalinkCopyReady">Copy</span>
<span :class="$style.permalinkCopyDone">Copied!</span>
</button>
<p :class="$style.note">
Note that permalinks are only available for L-systems from the predefined collections.
Any manual changes in L-system parameters are not preserved.
</p>
<h3>Creating shareable permalink for an L-system</h3>
<!-- eslint-disable vue/max-attributes-per-line -->
<ol :class="interfaceStyles.list">
<li><a href="https://docs.github.com/en/get-started/writing-on-github/editing-and-sharing-content-with-gists/creating-gists#creating-a-gist" target="_blank" rel="noopener">Create</a> a <em>public</em> gist on GitHub.</li>
<li>
Copy LSVG file content
<svg
:class="$style.copyButton"
height="16"
width="16"
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
@click="copyLSVG"
>
<title>Copy LSVG to clipboard</title>
<path d="M0 6.75C0 5.78.78 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .14.11.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z" />
<path d="M5 1.75C5 .78 5.78 0 6.75 0h7.5C15.22 0 16 .78 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .14.11.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />
</svg>
into the newly created public gist and save it as JSON.
</li>
<li>
Replace <b>{gist_id}</b> in the following hyperlink with the actual identifier of your gist<br>
<span :class="$style.permalink">https://amphiluke.github.io/lindsvg/?gist=<b>{gist_id}</b></span>.
</li>
</ol>
<p>Example: <a href="https://gist.github.com/Amphiluke/d90562f1aaf0f9ee28340c13ce13a6ca" target="_blank" rel="noopener">gist</a> &amp; <a href="https://amphiluke.github.io/lindsvg/?gist=d90562f1aaf0f9ee28340c13ce13a6ca" target="_blank">permalink</a>.</p>
<!-- eslint-enable vue/max-attributes-per-line -->
</form>
</section>
</template>
Expand Down Expand Up @@ -174,30 +166,14 @@ async function copyPermalink({target}) {
}
}
.permalinkField {
box-sizing: border-box;
height: 55px;
margin-top: 10px;
resize: vertical;
width: 100%;
}
.permalinkCopyButton {
margin: 5px 0;
width: 100%;
&.copySuccess {
pointer-events: none;
}
&:not(.copySuccess) .permalinkCopyDone,
&.copySuccess .permalinkCopyReady {
display: none;
}
.copyButton {
cursor: pointer;
fill: var(--color-accent);
margin-inline: 4px;
}
.note {
color: var(--color-on-surface-mid);
font-size: 0.85em;
.permalink {
color: var(--color-accent);
word-wrap: break-word;
}
</style>
83 changes: 83 additions & 0 deletions src/launchCtrl.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {useInterfaceStore} from "./stores/interface.mjs";
import {useLSystemStore} from "./stores/lSystem.mjs";
import {useCollectionsStore} from "./stores/collections.mjs";

export function processLaunchOptions() {
return processLaunchQueue() || processBundledPermalink() || processGistPermalink();
}

function extractQuery(...names) {
let url = new URL(location.href);
let params = Object.fromEntries(names.map((name) => [name, url.searchParams.get(name)]));
names.forEach((name) => url.searchParams.delete(name));
window.history.replaceState(null, "", url);
return params;
}

function processLaunchQueue() {
let launchParamsSupported = ("launchQueue" in window) && ("files" in window.LaunchParams.prototype);
/** @see https://github.com/WICG/web-app-launch/issues/92#issuecomment-1505562033 */
if (!launchParamsSupported || extractQuery("action").action !== "handleFile") {
return false;
}
/** @see https://github.com/WICG/file-handling/blob/main/explainer.md */
window.launchQueue.setConsumer(async ({files}) => {
if (!files.length) {
return;
}
try {
let blob = await files[0].getFile();
let config = JSON.parse(await blob.text());
let lSystemStore = useLSystemStore();
lSystemStore.setup(config);
lSystemStore.buildSVG();
} catch (error) {
useInterfaceStore().requestPopover({text: "Unfortunately, this file cannot be opened"});
console.error("Unable to open the file", error);
}
});
return true;
}

function processBundledPermalink() {
let {cid, lid} = extractQuery("cid", "lid");
if (!cid || !lid) {
return false;
}
let collectionsStore = useCollectionsStore();
collectionsStore.selectedCID = cid;
collectionsStore.selectedLID = lid;
let lSystemStore = useLSystemStore();
lSystemStore.setup(collectionsStore.selectedLSystem);
lSystemStore.buildSVG();
return true;
}

function processGistPermalink() {
let {gist} = extractQuery("gist");
if (!gist) {
return false;
}
fetch(`https://api.github.com/gists/${gist}`, {
method: "GET",
headers: {
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
},
mode: "cors",
credentials: "omit",
})
.then((response) => response.json())
.then((data) => {
let {content} = Object.values(data.files)[0];
let config = JSON.parse(content);
let lSystemStore = useLSystemStore();
lSystemStore.setup(config);
lSystemStore.buildSVG();
})
.catch((reason) => {
useInterfaceStore().requestPopover({text: "Unfortunately, this gist cannot be opened"});
console.error("Unable to open the gist", reason);
});
return true;
}
Loading

0 comments on commit 7aa9063

Please sign in to comment.