Skip to content
Closed
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
819 changes: 718 additions & 101 deletions app.js

Large diffs are not rendered by default.

106 changes: 91 additions & 15 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<link rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%23667eea'/><rect x='20' y='30' width='60' height='45' rx='5' fill='white'/><circle cx='50' cy='20' r='5' fill='white'/></svg>">

<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="styles.css?v=20260823.4">

<!-- Detect Tauri and add class for native styling -->
<script>
Expand Down Expand Up @@ -243,6 +243,10 @@ <h2>Screenshots</h2>
<span class="device-option-size">1242 × 2208</span>
</div>
<div class="output-size-divider"></div>
<div class="device-option" data-device="ipad-13">
<span class="device-option-name">iPad 13&quot;</span>
<span class="device-option-size">2064 × 2752</span>
</div>
<div class="device-option" data-device="ipad-12.9">
<span class="device-option-name">iPad 12.9"</span>
<span class="device-option-size">2048 × 2732</span>
Expand Down Expand Up @@ -632,6 +636,25 @@ <h2>Screenshots</h2>

<!-- Screenshot Tab -->
<div class="tab-content" id="tab-screenshot">
<div class="control-group">
<label class="control-label" for="device-instance-selector">Device Instance</label>
<select id="device-instance-selector" class="control-select" style="width: 100%; margin-bottom: 8px;">
<option value="primary">Device 1</option>
</select>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<button type="button" id="add-secondary-device" class="shared-3d-button" style="flex: 1; min-width: 150px;">
<span>+ Add second device</span>
</button>
<button type="button" id="remove-secondary-device" class="shared-3d-button" style="display: none; flex: 1; min-width: 130px;">
<span>Remove device</span>
</button>
<button type="button" id="replace-device-image" class="shared-3d-button" style="flex: 1 0 100%;">
<span>Replace selected device content</span>
</button>
<input type="file" id="replace-device-image-input" accept="image/*" hidden>
</div>
</div>

<div class="control-group">
<label class="control-label">Device Type</label>
<div class="btn-group" id="device-type-selector">
Expand All @@ -652,21 +675,62 @@ <h2>Screenshots</h2>
<label class="control-label">Rotation X (Tilt)</label>
<div class="control-row">
<input type="range" id="rotation-3d-x" min="-45" max="45" value="0">
<span class="range-value" id="rotation-3d-x-value">0°</span>
<div class="range-number-value">
<input type="number" id="rotation-3d-x-value" value="0" step="any"
aria-label="Rotation X in degrees">
<span>°</span>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label">Rotation Y (Turn)</label>
<div class="control-row">
<input type="range" id="rotation-3d-y" min="-45" max="45" value="0">
<span class="range-value" id="rotation-3d-y-value">0°</span>
<div class="range-number-value">
<input type="number" id="rotation-3d-y-value" value="0" step="any"
aria-label="Rotation Y in degrees">
<span>°</span>
</div>
</div>
</div>
<div class="control-group">
<label class="control-label">Rotation Z (Roll)</label>
<div class="control-row">
<input type="range" id="rotation-3d-z" min="-45" max="45" value="0">
<span class="range-value" id="rotation-3d-z-value">0°</span>
<div class="range-number-value">
<input type="number" id="rotation-3d-z-value" value="0" step="any"
aria-label="Rotation Z in degrees">
<span>°</span>
</div>
</div>
</div>
<div class="control-group shared-3d-control">
<label class="control-label">Multi-Screen Device</label>
<button type="button" id="shared-3d-toggle" class="shared-3d-button">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="5" width="7" height="14" rx="1.5"></rect>
<rect x="14" y="5" width="7" height="14" rx="1.5"></rect>
<path d="M10 12h4"></path>
</svg>
<span id="shared-3d-toggle-label">Continue onto next screen</span>
</button>
<p class="shared-3d-status" id="shared-3d-status">
Render one continuous perspective across two screenshots.
</p>
<div id="shared-3d-gap-control" class="shared-3d-gap-control" style="display: none;">
<label class="control-label" for="shared-3d-gap">Screenshot Gap</label>
<div class="control-row">
<input type="range" id="shared-3d-gap" min="0" max="20" step="0.1"
value="3.1">
<div class="range-number-value">
<input type="number" id="shared-3d-gap-value" min="0" value="3.1"
step="any" aria-label="Gap as a percentage of screenshot width">
<span>%</span>
</div>
</div>
<p class="shared-3d-status">Width skipped between the two rendered screenshots.</p>
</div>
</div>
</div>
Expand Down Expand Up @@ -753,15 +817,23 @@ <h2>Screenshots</h2>
<label class="control-label">Vertical Position</label>
<div class="control-row">
<input type="range" id="screenshot-y" min="-80" max="180" value="55">
<span class="range-value" id="screenshot-y-value">55%</span>
<div class="range-number-value">
<input type="number" id="screenshot-y-value" value="55" step="any"
aria-label="Vertical position percentage">
<span>%</span>
</div>
</div>
</div>

<div class="control-group">
<label class="control-label">Horizontal Position</label>
<div class="control-row">
<input type="range" id="screenshot-x" min="-80" max="180" value="50">
<span class="range-value" id="screenshot-x-value">50%</span>
<div class="range-number-value">
<input type="number" id="screenshot-x-value" value="50" step="any"
aria-label="Horizontal position percentage">
<span>%</span>
</div>
</div>
</div>

Expand All @@ -785,7 +857,11 @@ <h2>Screenshots</h2>
<label class="control-label">Tilt / Rotation</label>
<div class="control-row">
<input type="range" id="screenshot-rotation" min="-45" max="45" value="0">
<span class="range-value" id="screenshot-rotation-value">0°</span>
<div class="range-number-value">
<input type="number" id="screenshot-rotation-value" value="0" step="any"
aria-label="Device rotation in degrees">
<span>°</span>
</div>
</div>
</div>

Expand Down Expand Up @@ -2147,13 +2223,13 @@ <h2>Exporting Screenshots</h2>
</div>
</div>

<script src="llm.js"></script>
<script src="language-utils.js"></script>
<script src="magical-titles.js"></script>
<script src="lucide-icons.js"></script>
<script src="three-renderer.js"></script>
<script src="app.js"></script>
<script src="mcp-bridge.js"></script>
<script src="llm.js?v=20260823.4"></script>
<script src="language-utils.js?v=20260823.4"></script>
<script src="magical-titles.js?v=20260823.4"></script>
<script src="lucide-icons.js?v=20260823.4"></script>
<script src="three-renderer.js?v=20260823.4"></script>
<script src="app.js?v=20260823.4"></script>
<script src="mcp-bridge.js?v=20260823.4"></script>
</body>

</html>
</html>
116 changes: 115 additions & 1 deletion mcp-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

const knownOutputDevices = [
'iphone-6.9', 'iphone-6.7', 'iphone-6.5', 'iphone-5.5',
'ipad-12.9', 'ipad-11',
'ipad-13', 'ipad-12.9', 'ipad-11',
'android-phone', 'android-phone-hd', 'android-tablet-7', 'android-tablet-10',
'web-og', 'web-twitter', 'web-hero', 'web-feature', 'custom'
];
Expand Down Expand Up @@ -328,6 +328,7 @@
imageBackgrounds: true,
twoDimensionalMockups: true,
threeDimensionalMockups: true,
multipleDeviceInstancesPerScreenshot: true,
elements: true,
popouts: true,
aiTranslationViaAppSettings: true,
Expand Down Expand Up @@ -360,6 +361,44 @@
});
},

async importPortableProject({ project }) {
return invoke('importPortableProject', async () => {
if (!project || typeof project !== 'object') {
throw new Error('A portable AppScreen project object is required.');
}
if (!db) throw new Error('The project database is not ready.');

const dump = normalizeProjectImport(project);
const currentRecord = dump.meta?.find(record => record.key === 'currentProject');
const importedProjectId = currentRecord?.value;
if (!importedProjectId) throw new Error('The imported project has no project id.');
const previousProjectId = currentProjectId;

for (const storeName of Object.keys(dump)) {
if (!db.objectStoreNames.contains(storeName)) continue;
const tx = db.transaction(storeName, 'readwrite');
const store = tx.objectStore(storeName);
for (const record of dump[storeName]) store.put(record);
await new Promise((resolve, reject) => {
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
}

await loadProjectsMeta();
// loadProjectsMeta adopts the imported id immediately. Restore the
// previous id so switchProject does not save the old in-memory
// state over the newly imported database record.
currentProjectId = previousProjectId;
await switchProject(importedProjectId);
return {
id: currentProjectId,
screenshotCount: state.screenshots.length,
languages: state.projectLanguages.slice()
};
});
},

async switchProject({ projectId }) {
return invoke('switchProject', async () => {
if (!projects.some((p) => p.id === projectId)) throw new Error(`Unknown project id: ${projectId}`);
Expand Down Expand Up @@ -475,6 +514,34 @@
});
},

async setSecondaryDevice({ index = state.selectedIndex, device } = {}) {
return invoke('setSecondaryDevice', async () => {
const s = ensureScreenshot(index);
if (!device?.screenshot) throw new Error('Secondary device settings are required.');
const localizedImages = {};
for (const [language, localized] of Object.entries(device.localizedImages || {})) {
const source = localized?.src || localized?.dataUrl;
if (!source) continue;
const img = await loadImageFromDataUrl(source);
localizedImages[language] = {
image: img,
src: source,
name: localized.name || 'Device 2 content'
};
ensureLanguages([language]);
}
s.secondaryDevice = {
name: device.name || 'Device 2',
screenshot: clonePlain(device.screenshot),
localizedImages
};
state.selectedIndex = Number(index);
selectedDeviceSlot = 'secondary';
refresh();
return { index: state.selectedIndex, secondaryDevice: stripRuntimeImages({ secondaryDevice: s.secondaryDevice }).secondaryDevice };
});
},

async removeScreenshot({ index }) {
return invoke('removeScreenshot', async () => {
const i = ensureIndex(index);
Expand Down Expand Up @@ -532,6 +599,53 @@
});
},

async setShared3DPair({ index = state.selectedIndex, enabled = true, gapPercent = null } = {}) {
return invoke('setShared3DPair', async () => {
const firstIndex = ensureIndex(index);
if (firstIndex >= state.screenshots.length - 1) {
throw new Error('A multi-screen device needs a following screenshot.');
}

const first = ensureScreenshot(firstIndex);
const second = ensureScreenshot(firstIndex + 1);
const currentGroupId = first.screenshot.shared3DView?.groupId;
const currentMembers = typeof getShared3DGroupMembers === 'function'
? getShared3DGroupMembers(currentGroupId).sort((a, b) => a.index - b.index)
: [];
const isRequestedPair = currentMembers.length === 2
&& currentMembers[0].index === firstIndex
&& currentMembers[1].index === firstIndex + 1;

if (enabled && !isRequestedPair) {
if (currentGroupId && typeof unlinkShared3DGroup === 'function') {
unlinkShared3DGroup(currentGroupId);
}
const secondGroupId = second.screenshot.shared3DView?.groupId;
if (secondGroupId && typeof unlinkShared3DGroup === 'function') {
unlinkShared3DGroup(secondGroupId);
}

state.selectedIndex = firstIndex;
toggleShared3DWithNext();
} else if (!enabled && currentGroupId && typeof unlinkShared3DGroup === 'function') {
unlinkShared3DGroup(currentGroupId);
}

if (enabled && gapPercent !== null && typeof setShared3DGapPercent === 'function') {
state.selectedIndex = firstIndex;
setShared3DGapPercent(gapPercent);
}

state.selectedIndex = firstIndex;
refresh();
return {
enabled: Boolean(first.screenshot.shared3DView),
indices: [firstIndex, firstIndex + 1],
screenshots: [clonePlain(first.screenshot), clonePlain(second.screenshot)]
};
});
},

async applyPositionPreset({ index = state.selectedIndex, preset }) {
return invoke('applyPositionPreset', async () => {
state.selectedIndex = ensureIndex(index);
Expand Down
20 changes: 10 additions & 10 deletions mcp-server/package-lock.json

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

4 changes: 2 additions & 2 deletions mcp-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.29.0",
"playwright": "^1.56.0",
"playwright": "^1.62.1",
"zod": "^3.25.0"
},
"devDependencies": {
"@types/node": "latest",
"tsx": "latest",
"typescript": "latest"
}
}
}
Loading