Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>SphereQuickLook</string>
<key>CFBundleExecutable</key>
<string>SphereQuickLook</string>
<key>CFBundleIdentifier</key>
<string>com.tmshv.sphere.quicklook</string>
<key>CFBundleName</key>
<string>SphereQuickLook</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>QLSupportedContentTypes</key>
<array>
<string>com.tmshv.sphere.geojson</string>
</array>
<key>QLSupportsSearchableItems</key>
<false/>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.quicklook.preview</string>
<key>NSExtensionPrincipalClass</key>
<string>PreviewViewController</string>
</dict>
</dict>
</plist>
36 changes: 36 additions & 0 deletions extensions/quicklook/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>SphereQuickLook</string>
<key>CFBundleExecutable</key>
<string>SphereQuickLook</string>
<key>CFBundleIdentifier</key>
<string>com.tmshv.sphere.quicklook</string>
<key>CFBundleName</key>
<string>SphereQuickLook</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>QLSupportedContentTypes</key>
<array>
<string>com.tmshv.sphere.geojson</string>
</array>
<key>QLSupportsSearchableItems</key>
<false/>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.quicklook.preview</string>
<key>NSExtensionPrincipalClass</key>
<string>PreviewViewController</string>
</dict>
</dict>
</plist>
64 changes: 64 additions & 0 deletions extensions/quicklook/PreviewViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Cocoa
import Quartz
import WebKit

class PreviewViewController: NSViewController, QLPreviewingController {

var webView: WKWebView!
private var navigationHandler: NavigationHandler?

override var nibName: NSNib.Name? {
return NSNib.Name("PreviewViewController")
}

override func loadView() {
let config = WKWebViewConfiguration()
webView = WKWebView(frame: .zero, configuration: config)
webView.translatesAutoresizingMaskIntoConstraints = false
self.view = webView
}

func preparePreviewOfFile(at url: URL, completionHandler handler: @escaping (Error?) -> Void) {
guard let resourcesURL = Bundle.main.resourceURL else {
handler(NSError(domain: "SphereQuickLook", code: 1, userInfo: [NSLocalizedDescriptionKey: "Cannot find bundle resources"]))
return
}

let indexURL = resourcesURL.appendingPathComponent("index.html")

let nav = NavigationHandler { [weak self] in
guard let self = self else { return }
do {
let data = try Data(contentsOf: url)
guard let jsonString = String(data: data, encoding: .utf8) else {
handler(NSError(domain: "SphereQuickLook", code: 2, userInfo: [NSLocalizedDescriptionKey: "Cannot decode file as UTF-8"]))
return
}
let escaped = jsonString
.replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "`", with: "\\`")
let js = "window.loadGeoJSON(`\(escaped)`)"
self.webView.evaluateJavaScript(js) { _, error in
handler(error)
}
} catch {
handler(error)
}
}
navigationHandler = nav
webView.navigationDelegate = nav
webView.loadFileURL(indexURL, allowingReadAccessTo: resourcesURL)
}
}

class NavigationHandler: NSObject, WKNavigationDelegate {
let onFinish: () -> Void

init(onFinish: @escaping () -> Void) {
self.onFinish = onFinish
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
onFinish()
}
}
149 changes: 149 additions & 0 deletions extensions/quicklook/Resources/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoJSON Preview</title>
<link rel="stylesheet" href="maplibre-gl.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; }
#map { width: 100%; height: 100%; }
</style>
</head>
<body>
<div id="map"></div>
<script src="maplibre-gl.js"></script>
<script>
const map = new maplibregl.Map({
container: "map",
style: {
version: 8,
sources: {
osm: {
type: "raster",
tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
tileSize: 256,
attribution: "&copy; <a href='https://www.openstreetmap.org/copyright'>OpenStreetMap</a> contributors",
maxzoom: 19
}
},
layers: [
{
id: "osm",
type: "raster",
source: "osm"
}
]
},
center: [0, 0],
zoom: 1
})

function calcBbox(geojson) {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity

function processCoords(coords) {
if (typeof coords[0] === "number") {
const [x, y] = coords
if (x < minX) minX = x
if (y < minY) minY = y
if (x > maxX) maxX = x
if (y > maxY) maxY = y
} else {
coords.forEach(processCoords)
}
}

function processGeometry(geom) {
if (!geom) return
if (geom.type === "GeometryCollection") {
geom.geometries.forEach(processGeometry)
} else if (geom.coordinates) {
processCoords(geom.coordinates)
}
}

function processFeature(feature) {
if (feature.type === "Feature") {
processGeometry(feature.geometry)
} else if (feature.type === "FeatureCollection") {
feature.features.forEach(processFeature)
} else {
processGeometry(feature)
}
}

processFeature(geojson)

if (!isFinite(minX)) return [-180, -90, 180, 90]
return [minX, minY, maxX, maxY]
}

window.loadGeoJSON = function(input) {
let data
try {
data = typeof input === "string" ? JSON.parse(input) : input
} catch (e) {
console.error("Failed to parse GeoJSON:", e)
return
}

if (map.getSource("geojson-data")) {
map.removeLayer("geojson-fill")
map.removeLayer("geojson-line")
map.removeLayer("geojson-circle")
map.removeSource("geojson-data")
}

map.addSource("geojson-data", {
type: "geojson",
data: data
})

map.addLayer({
id: "geojson-fill",
type: "fill",
source: "geojson-data",
filter: ["==", ["geometry-type"], "Polygon"],
paint: {
"fill-color": "#3388ff",
"fill-opacity": 0.3
}
})

map.addLayer({
id: "geojson-line",
type: "line",
source: "geojson-data",
filter: ["in", ["geometry-type"], ["literal", ["LineString", "MultiLineString", "Polygon", "MultiPolygon"]]],
paint: {
"line-color": "#3388ff",
"line-width": 2
}
})

map.addLayer({
id: "geojson-circle",
type: "circle",
source: "geojson-data",
filter: ["in", ["geometry-type"], ["literal", ["Point", "MultiPoint"]]],
paint: {
"circle-color": "#3388ff",
"circle-radius": 6,
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 1.5
}
})

const [minX, minY, maxX, maxY] = calcBbox(data)
const pad = 0.1 * Math.max(maxX - minX, maxY - minY, 0.01)

map.fitBounds(
[[minX - pad, minY - pad], [maxX + pad, maxY + pad]],
{ padding: 40, duration: 0, maxZoom: 16 }
)
}
</script>
</body>
</html>
1 change: 1 addition & 0 deletions extensions/quicklook/Resources/maplibre-gl.css

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions extensions/quicklook/Resources/maplibre-gl.js

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions extensions/quicklook/SphereQuickLook.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
</dict>
</plist>
66 changes: 66 additions & 0 deletions extensions/quicklook/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
RESOURCES_DIR="$SCRIPT_DIR/Resources"
BUILD_DIR="$SCRIPT_DIR/.build"
APPEX_NAME="SphereQuickLook.appex"
APPEX_PATH="$BUILD_DIR/$APPEX_NAME"

# Copy MapLibre assets into Resources
MAPLIBRE_DIST="$REPO_ROOT/node_modules/maplibre-gl/dist"
echo "Copying MapLibre GL assets..."
cp "$MAPLIBRE_DIST/maplibre-gl.js" "$RESOURCES_DIR/maplibre-gl.js"
cp "$MAPLIBRE_DIST/maplibre-gl.css" "$RESOURCES_DIR/maplibre-gl.css"

# Create bundle structure
echo "Creating .appex bundle..."
rm -rf "$APPEX_PATH"
mkdir -p "$APPEX_PATH/Contents/MacOS"
mkdir -p "$APPEX_PATH/Contents/Resources"

# Compile Swift source
echo "Compiling PreviewViewController.swift..."
swiftc \
-module-name SphereQuickLook \
-parse-as-library \
-target arm64-apple-macos12.0 \
-framework Cocoa \
-framework WebKit \
-framework Quartz \
-Xlinker -bundle \
-o "$APPEX_PATH/Contents/MacOS/SphereQuickLook" \
"$SCRIPT_DIR/PreviewViewController.swift"

# Copy plist and resources
cp "$SCRIPT_DIR/Info.plist" "$APPEX_PATH/Contents/Info.plist"
cp "$RESOURCES_DIR/index.html" "$APPEX_PATH/Contents/Resources/index.html"
cp "$RESOURCES_DIR/maplibre-gl.js" "$APPEX_PATH/Contents/Resources/maplibre-gl.js"
cp "$RESOURCES_DIR/maplibre-gl.css" "$APPEX_PATH/Contents/Resources/maplibre-gl.css"

# Ad-hoc sign with entitlements
echo "Signing $APPEX_NAME..."
codesign \
--force \
--sign - \
--entitlements "$SCRIPT_DIR/SphereQuickLook.entitlements" \
"$APPEX_PATH"

# Install into the Tauri app bundle
APP_BUNDLE="$REPO_ROOT/src-tauri/target/release/bundle/macos/Sphere.app"
PLUGINS_DIR="$APP_BUNDLE/Contents/PlugIns"

if [ -d "$APP_BUNDLE" ]; then
echo "Installing into $PLUGINS_DIR..."
mkdir -p "$PLUGINS_DIR"
rm -rf "$PLUGINS_DIR/$APPEX_NAME"
cp -R "$APPEX_PATH" "$PLUGINS_DIR/$APPEX_NAME"
echo "Installed: $PLUGINS_DIR/$APPEX_NAME"
else
echo "Warning: Sphere.app not found at $APP_BUNDLE"
echo "Run 'npm run tauri build' first, then re-run this script."
echo "Built .appex is at: $APPEX_PATH"
fi

echo "Done."
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"test": "vitest run",
"test:watch": "vitest watch",
"coverage": "vitest run --coverage",
"tauri": "tauri"
"tauri": "tauri",
"build:quicklook": "bash extensions/quicklook/build.sh"
},
"dependencies": {
"@hyvilo/maplibre-gl-draw": "^1.0.0",
Expand Down
Loading
Loading