| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796 |
- <!DOCTYPE html>
- <meta name="apple-mobile-web-app-capable" content="yes" />
- <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1" />
- <html>
- <head>
- <meta charset="UTF-8">
- <meta name="theme-color" content="#4b75ff">
- <title>Photo Viewer</title>
- <script src="../script/jquery.min.js"></script>
- <script src="../script/ao_module.js"></script>
- <script src="constants.js"></script>
- <link rel="manifest" href="manifest.json">
- <style>
- body{
- margin: 0px !important;
- background:#1a1a1a;
- overflow: hidden;
- display: flex;
- }
-
- #imageContainer{
- flex: 1;
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: margin-right 0.3s ease;
- }
-
- #imageContainer.editing{
- margin-right: 320px;
- }
- .arrow{
- width: 2em;
- opacity: 0.5;
- position: fixed;
- top: calc(50% - 1em);
- cursor: pointer;
- }
- .left.arrow{
- left: 2em;
- }
- .right.arrow{
- right: 2em;
- }
- #img{
- transition: transform 0.5s;
- }
- .edit{
- width: 2em;
- opacity: 0.5;
- position: fixed;
- bottom: 1em;
- cursor: pointer;
- left: 1em;
- }
- .edit:hover{
- opacity: 1;
- }
- #editPanel{
- position: fixed;
- right: 0;
- top: 0;
- width: 320px;
- height: 100vh;
- background: rgba(26, 26, 26, 0.95);
- backdrop-filter: blur(10px);
- color: white;
- padding: 20px;
- box-sizing: border-box;
- transform: translateX(100%);
- transition: transform 0.3s ease;
- z-index: 1000;
- overflow-y: auto;
- flex-shrink: 0;
- }
- #editPanel.active{
- transform: translateX(0);
- }
- .edit-section{
- margin-bottom: 25px;
- padding-bottom: 20px;
- border-bottom: 1px solid rgba(255,255,255,0.1);
- }
- .edit-section:last-child{
- border-bottom: none;
- }
- .edit-section h3{
- margin: 0 0 15px 0;
- font-size: 14px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: #4b75ff;
- }
- .button-group{
- display: flex;
- gap: 10px;
- margin-top: 10px;
- }
- .edit-button{
- flex: 1;
- padding: 10px;
- background: #4b75ff;
- color: white;
- border: none;
- border-radius: 5px;
- cursor: pointer;
- font-size: 13px;
- transition: background 0.2s;
- }
- .edit-button:hover{
- background: #3d5fd8;
- }
- .edit-button.secondary{
- background: rgba(255,255,255,0.1);
- }
- .edit-button.secondary:hover{
- background: rgba(255,255,255,0.2);
- }
- #cropOverlay{
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- display: none;
- z-index: 999;
- }
- #cropOverlay.active{
- display: block;
- }
- #cropBox{
- position: absolute;
- border: 2px solid #4b75ff;
- box-shadow: 0 0 0 9999px rgba(0,0,0,0.5);
- cursor: move;
- }
- .crop-handle{
- position: absolute;
- width: 20px;
- height: 20px;
- background: white;
- border: 2px solid #4b75ff;
- border-radius: 50%;
- cursor: move;
- }
- .crop-handle.top{
- top: -10px;
- left: 50%;
- transform: translateX(-50%);
- cursor: ns-resize;
- }
- .crop-handle.bottom{
- bottom: -10px;
- left: 50%;
- transform: translateX(-50%);
- cursor: ns-resize;
- }
- .crop-handle.left{
- left: -10px;
- top: 50%;
- transform: translateY(-50%);
- cursor: ew-resize;
- }
- .crop-handle.right{
- right: -10px;
- top: 50%;
- transform: translateY(-50%);
- cursor: ew-resize;
- }
- #closeEditPanel{
- position: absolute;
- top: 15px;
- right: 15px;
- width: 30px;
- height: 30px;
- background: rgba(255,255,255,0.1);
- border: none;
- border-radius: 50%;
- color: white;
- font-size: 20px;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.2s;
- }
- #closeEditPanel:hover{
- background: rgba(255,255,255,0.2);
- }
- </style>
- </head>
- <body>
- <div id="imageContainer">
- <img id="img" style="max-height: 100vh;max-width: 100%;">
- <img class="left arrow" style="display:none;" onclick="previousImage();" src="embedded/arrow-left.svg">
- <img class="right arrow" style="display:none;" onclick="nextImage();" src="embedded/arrow-right.svg">
- <img class="edit" onclick="toggleEditPanel();" src="embedded/edit.svg" title="Edit Image">
- </div>
-
- <!-- Edit Panel -->
- <div id="editPanel">
- <button id="closeEditPanel" onclick="toggleEditPanel();">×</button>
- <h2 style="margin-top: 0; font-size: 18px;">Edit Image</h2>
-
- <!-- Crop Section -->
- <div class="edit-section">
- <h3>Crop Image</h3>
- <div class="button-group">
- <button class="edit-button" onclick="startCrop();">Start Crop</button>
- <button class="edit-button secondary" onclick="cancelCrop();">Cancel</button>
- </div>
- <button class="edit-button" style="margin-top: 10px; width: 100%;" onclick="applyCrop();">Apply Crop</button>
- </div>
-
- <!-- Rotation Section -->
- <div class="edit-section">
- <h3>Rotate</h3>
- <div class="button-group">
- <button class="edit-button" onclick="rotateImage(-90);">↺ Left</button>
- <button class="edit-button" onclick="rotateImage(90);">↻ Right</button>
- </div>
- </div>
-
- <!-- Save Section -->
- <div class="edit-section">
- <h3>Save</h3>
- <button class="edit-button" style="width: 100%; margin-bottom: 10px;" onclick="saveToArozOS();">Save to ArozOS</button>
- <button class="edit-button secondary" style="width: 100%;" onclick="downloadImage();">Download to Device</button>
- </div>
-
- <!-- Reset Section -->
- <div class="edit-section">
- <button class="edit-button secondary" style="width: 100%;" onclick="resetEdits();">Reset All Changes</button>
- </div>
- </div>
-
- <!-- Crop Overlay -->
- <div id="cropOverlay">
- <div id="cropBox">
- <div class="crop-handle top"></div>
- <div class="crop-handle bottom"></div>
- <div class="crop-handle left"></div>
- <div class="crop-handle right"></div>
- </div>
- </div>
- <script>
- //Get file playback info from hash
- var playbackFile = ao_module_loadInputFiles();
- var nearbyFileList = [];
- var currentImageURL = "";
- var currentImageFilename = "";
- var currentViewingIndex = 0;
- var initMargin = [];
- var currentMargin = [];
-
- // Edit functionality variables
- var rotation = 0;
- var isCropping = false;
- var isEditMode = false;
- var cropData = null;
- var canvas = document.createElement('canvas');
- var ctx = canvas.getContext('2d');
- //Only handle one file
- playbackFile = playbackFile[0];
- loadImage(playbackFile.filename, playbackFile.filepath);
-
- $(window).on("resize ", function() {
- updateImgSize();
- });
- /*
- Edit Panel Functions
- */
- let interval;
- function toggleEditPanel(){
- var panel = document.getElementById('editPanel');
- var container = document.getElementById('imageContainer');
- panel.classList.toggle('active');
- container.classList.toggle('editing');
- isEditMode = panel.classList.contains('active');
- if (interval){
- clearInterval(interval);
- }
- interval = setInterval(function(){
- updateImgSize();
- }, 1);
- setTimeout(function(){
- clearInterval(interval);
- }, 300);
- }
- function applyAllFilters(){
- var transformString = `rotate(${rotation}deg)`;
- var img = document.getElementById('img');
- img.style.transform = transformString;
- }
- function rotateImage(degrees){
- rotation += degrees;
- applyAllFilters();
- }
- function resetEdits(){
- rotation = 0;
- cancelCrop();
- document.getElementById('img').src = currentImageURL;
- cropData = null;
- applyAllFilters();
- updateImgSize();
- }
- /*
- Crop Functions
- */
- function startCrop(){
- if (isCropping) return;
-
- isCropping = true;
- var overlay = document.getElementById('cropOverlay');
- var cropBox = document.getElementById('cropBox');
- var img = document.getElementById('img');
-
- // Get image position and size
- var rect = img.getBoundingClientRect();
-
- // Initialize crop box to center of image (50% size)
- var cropWidth = rect.width * 0.6;
- var cropHeight = rect.height * 0.6;
- var cropLeft = rect.left + (rect.width - cropWidth) / 2;
- var cropTop = rect.top + (rect.height - cropHeight) / 2;
-
- cropBox.style.left = cropLeft + 'px';
- cropBox.style.top = cropTop + 'px';
- cropBox.style.width = cropWidth + 'px';
- cropBox.style.height = cropHeight + 'px';
-
- overlay.classList.add('active');
-
- // Add drag functionality to handles
- setupCropHandles();
- }
- function setupCropHandles(){
- var cropBox = document.getElementById('cropBox');
- var handles = cropBox.querySelectorAll('.crop-handle');
-
- // Remove existing listeners by cloning
- var newCropBox = cropBox.cloneNode(true);
- cropBox.parentNode.replaceChild(newCropBox, cropBox);
- cropBox = newCropBox;
- handles = cropBox.querySelectorAll('.crop-handle');
-
- // Add drag functionality to the crop box itself
- cropBox.addEventListener('mousedown', function(e){
- // Check if clicking on the box itself, not a handle
- if (e.target === cropBox){
- e.preventDefault();
- var startX = e.clientX;
- var startY = e.clientY;
- var startLeft = parseInt(cropBox.style.left);
- var startTop = parseInt(cropBox.style.top);
- var img = document.getElementById('img');
- var imgRect = img.getBoundingClientRect();
- var boxWidth = parseInt(cropBox.style.width);
- var boxHeight = parseInt(cropBox.style.height);
-
- function onMouseMove(e){
- var dx = e.clientX - startX;
- var dy = e.clientY - startY;
- var newLeft = startLeft + dx;
- var newTop = startTop + dy;
-
- // Constrain to image boundaries
- if (newLeft < imgRect.left) newLeft = imgRect.left;
- if (newTop < imgRect.top) newTop = imgRect.top;
- if (newLeft + boxWidth > imgRect.right) newLeft = imgRect.right - boxWidth;
- if (newTop + boxHeight > imgRect.bottom) newTop = imgRect.bottom - boxHeight;
-
- cropBox.style.left = newLeft + 'px';
- cropBox.style.top = newTop + 'px';
- }
-
- function onMouseUp(){
- document.removeEventListener('mousemove', onMouseMove);
- document.removeEventListener('mouseup', onMouseUp);
- }
-
- document.addEventListener('mousemove', onMouseMove);
- document.addEventListener('mouseup', onMouseUp);
- }
- });
-
- // Add handle resize functionality
- handles.forEach(function(handle){
- handle.addEventListener('mousedown', function(e){
- e.preventDefault();
- e.stopPropagation(); // Prevent triggering box drag
- var startX = e.clientX;
- var startY = e.clientY;
- var startLeft = parseInt(cropBox.style.left);
- var startTop = parseInt(cropBox.style.top);
- var startWidth = parseInt(cropBox.style.width);
- var startHeight = parseInt(cropBox.style.height);
- var handleClass = handle.className.split(' ')[1];
- var img = document.getElementById('img');
- var imgRect = img.getBoundingClientRect();
- var minSize = 20; // Minimum crop box size
-
- function onMouseMove(e){
- var dx = e.clientX - startX;
- var dy = e.clientY - startY;
-
- if (handleClass === 'top'){
- var newTop = startTop + dy;
- var newHeight = startHeight - dy;
- // Constrain to image boundaries and minimum size
- if (newTop < imgRect.top) {
- newTop = imgRect.top;
- newHeight = startTop + startHeight - imgRect.top;
- }
- if (newHeight >= minSize){
- cropBox.style.top = newTop + 'px';
- cropBox.style.height = newHeight + 'px';
- }
- } else if (handleClass === 'bottom'){
- var newHeight = startHeight + dy;
- var maxHeight = imgRect.bottom - startTop;
- if (newHeight > maxHeight) newHeight = maxHeight;
- if (newHeight >= minSize){
- cropBox.style.height = newHeight + 'px';
- }
- } else if (handleClass === 'left'){
- var newLeft = startLeft + dx;
- var newWidth = startWidth - dx;
- // Constrain to image boundaries and minimum size
- if (newLeft < imgRect.left) {
- newLeft = imgRect.left;
- newWidth = startLeft + startWidth - imgRect.left;
- }
- if (newWidth >= minSize){
- cropBox.style.left = newLeft + 'px';
- cropBox.style.width = newWidth + 'px';
- }
- } else if (handleClass === 'right'){
- var newWidth = startWidth + dx;
- var maxWidth = imgRect.right - startLeft;
- if (newWidth > maxWidth) newWidth = maxWidth;
- if (newWidth >= minSize){
- cropBox.style.width = newWidth + 'px';
- }
- }
- }
-
- function onMouseUp(){
- document.removeEventListener('mousemove', onMouseMove);
- document.removeEventListener('mouseup', onMouseUp);
- }
-
- document.addEventListener('mousemove', onMouseMove);
- document.addEventListener('mouseup', onMouseUp);
- });
- });
- }
- function cancelCrop(){
- isCropping = false;
- document.getElementById('cropOverlay').classList.remove('active');
- }
- function applyCrop(){
- if (!isCropping) return;
-
- var cropBox = document.getElementById('cropBox');
- var img = document.getElementById('img');
- var imgRect = img.getBoundingClientRect();
- var cropRect = cropBox.getBoundingClientRect();
-
- // Calculate crop coordinates relative to the image
- var scaleX = img.naturalWidth / imgRect.width;
- var scaleY = img.naturalHeight / imgRect.height;
-
- cropData = {
- x: (cropRect.left - imgRect.left) * scaleX,
- y: (cropRect.top - imgRect.top) * scaleY,
- width: cropRect.width * scaleX,
- height: cropRect.height * scaleY
- };
-
- // Apply crop visually
- canvas.width = cropData.width;
- canvas.height = cropData.height;
-
- var tempImg = new Image();
- tempImg.crossOrigin = 'anonymous';
- tempImg.onload = function(){
- ctx.drawImage(tempImg, cropData.x, cropData.y, cropData.width, cropData.height, 0, 0, cropData.width, cropData.height);
- img.src = canvas.toDataURL('image/png');
- cancelCrop();
- };
- tempImg.src = currentImageURL;
- }
- /*
- Save Functions
- */
- function getEditedImageBlob(callback){
- var img = document.getElementById('img');
-
- // Create a new canvas with all edits applied
- var editCanvas = document.createElement('canvas');
- var editCtx = editCanvas.getContext('2d');
-
- var tempImg = new Image();
- tempImg.crossOrigin = 'anonymous';
- tempImg.onload = function(){
- // Handle rotation
- if (rotation % 180 !== 0){
- editCanvas.width = tempImg.height;
- editCanvas.height = tempImg.width;
- } else {
- editCanvas.width = tempImg.width;
- editCanvas.height = tempImg.height;
- }
-
- editCtx.translate(editCanvas.width / 2, editCanvas.height / 2);
- editCtx.rotate(rotation * Math.PI / 180);
-
- editCtx.drawImage(tempImg, -tempImg.width / 2, -tempImg.height / 2);
-
- editCanvas.toBlob(function(blob){
- callback(blob);
- }, 'image/png');
- };
- tempImg.src = img.src;
- }
- let savePendingblob = null;
- function saveToArozOS(){
- getEditedImageBlob(function(blob){
- // Get current file's directory
- var currentDir = playbackFile.filepath.substring(0, playbackFile.filepath.lastIndexOf('/'));
- savePendingblob = blob;
- ao_module_openFileSelector(fileSelected, currentDir, 'new', false, {
- defaultName: 'edited_' + currentImageFilename
- });
- });
- }
- function fileSelected(selectedFiles){
- if (selectedFiles.length === 0){
- console.log('Save cancelled');
- return;
- }
-
- var savePath = selectedFiles[0].filepath;
- savePath = savePath.split("/");
- savePath.pop();
- savePath = savePath.join("/") + "/";
- var saveFilename = selectedFiles[0].filename;
- // Upload the edited image using ao_module_uploadFile
- var file = new File([savePendingblob], currentImageFilename, { type: 'image/png' });
- savePendingblob = null;
-
- ao_module_uploadFile(
- file,
- savePath,
- function(response){
- alert('Image saved successfully!');
- },
- undefined,
- function(status){
- alert('Failed to save image. Error code: ' + status);
- }
- );
- }
- function downloadImage(){
- getEditedImageBlob(function(blob){
- var url = URL.createObjectURL(blob);
- var a = document.createElement('a');
- a.href = url;
- a.download = 'edited_' + currentImageFilename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- });
- }
- //Load the nearby image files and allow swapping using <- and -> key
- function loadNearbyFiles(filepath){
- ao_module_agirun("Photo/embedded/listNearbyImage.js", {
- path: filepath
- }, function(data){
- if (data.error != undefined){
- alert(data.error);
- }else{
- nearbyFileList = data;
- $(".arrow").css("display", "");
- //Track which index currently the user is viewing
- for (var i = 0; i < nearbyFileList.length; i++){
- var thisPath = nearbyFileList[i];
- if (thisPath == filepath.split("\\").join("/")){
- currentViewingIndex = i;
- }
- }
- }
- })
- }
- function nextImage(){
- // Don't allow navigation when in edit mode
- if (isEditMode) return;
-
- nextPhoto = currentViewingIndex + 1;
- if (nextPhoto > nearbyFileList.length - 1){
- nextPhoto = nearbyFileList.length - 1;
- }
- var filepath = nearbyFileList[nextPhoto];
- var filename = filepath.split('/').pop();
- if (nextPhoto != currentViewingIndex){
- //Change in photo index
- loadImage(filename, filepath);
- currentViewingIndex = nextPhoto;
- }
- }
- function previousImage(){
- // Don't allow navigation when in edit mode
- if (isEditMode) return;
-
- nextPhoto = currentViewingIndex - 1;
- if (nextPhoto < 0){
- nextPhoto = 0;
- }
- var filepath = nearbyFileList[nextPhoto];
- var filename = filepath.split('/').pop();
- if (nextPhoto != currentViewingIndex){
- //Change in photo index
- loadImage(filename, filepath);
- currentViewingIndex = nextPhoto;
- }
- }
- //Bind arrow key events
- $("body").on("keydown", function(e){
- // Don't allow navigation when in edit mode
- if (isEditMode) return;
-
- var nextPhoto = currentViewingIndex;
- if (e.keyCode == 37){
- //<-
- if (nearbyFileList.length > 0){
- previousImage();
- }
- }else if (e.keyCode == 39){
- //->
- if (nearbyFileList.length > 0){
- nextImage();
- }
- }else{
- //Invalid keycode to operate
- return;
- }
- })
- loadNearbyFiles(playbackFile.filepath);
- async function loadImage(filename, filepath){
- $("#img").hide();
- ao_module_setWindowTitle("Photo - " + filename);
- // Backend handles RAW files automatically
- $("#img").attr("src", '../media?file=' + encodeURIComponent(filepath));
- currentImageURL = '../media?file=' + encodeURIComponent(filepath);
- currentImageFilename = filename;
- //realigin to center
- $('#img').on('load', function() {
- // Reset all edits when loading new image
- rotation = 0;
- applyAllFilters();
- updateImgSize();
- $("#img").show();
- });
- }
- function updateImgSize() {
- $('#img').css("margin-top", (window.innerHeight - $("#img").height()) / 2);
- initMargin = [(window.innerWidth - $("#img").width()) / 2, (window.innerHeight - $("#img").height()) / 2];
- currentMargin = initMargin;
- }
- //Touch gesture detections
- document.addEventListener('touchstart', handleTouchStart, false);
- document.addEventListener('touchmove', handleTouchMove, false);
- var xDown = null;
- var yDown = null;
- function getTouches(evt) {
- return evt.touches || // browser API
- evt.originalEvent.touches; // jQuery
- }
-
- function handleTouchStart(evt) {
- const firstTouch = getTouches(evt)[0];
- xDown = firstTouch.clientX;
- yDown = firstTouch.clientY;
- };
- function handleTouchMove(evt) {
- if ( ! xDown || ! yDown ) {
- return;
- }
- var xUp = evt.touches[0].clientX;
- var yUp = evt.touches[0].clientY;
- var xDiff = xDown - xUp;
- var yDiff = yDown - yUp;
-
- if ( Math.abs( xDiff ) > Math.abs( yDiff ) ) {/*most significant*/
- if ( xDiff > 0 ) {
- /* right swipe */
- nextImage();
- } else {
- /* left swipe */
- previousImage();
- }
- } else {
- if ( yDiff > 0 ) {
- /* down swipe */
- } else {
- /* up swipe */
- }
- }
-
- /* reset values */
- xDown = null;
- yDown = null;
-
- };
- function isZoomed(){
- return window.matchMedia('(max--moz-device-pixel-ratio:0.99), (min--moz-device-pixel-ratio:1.01)').matches;
- }
- </script>
- </body>
- </html>
|