diff --git a/src/components/audioPlayer/index.js b/src/components/audioPlayer/index.js new file mode 100644 index 000000000..33b552dd0 --- /dev/null +++ b/src/components/audioPlayer/index.js @@ -0,0 +1,147 @@ +import "./style.scss"; + +export default class AudioPlayer { + constructor(container) { + this.container = container; + this.audio = new Audio(); + this.isPlaying = false; + this.initializeUI(); + this.initializeEvents(); + this.cleanup = this.cleanup.bind(this); + } + + initializeUI() { + const auidoPlayer = ( +
+ + +
+
+
+
+ +
0:00
+ +
+ +
+
+ ); + + this.container.appendChild(auidoPlayer); + + this.elements = { + playBtn: this.container.querySelector(".play-btn"), + playIcon: this.container.querySelector(".play-btn .icon"), + timeline: this.container.querySelector(".timeline"), + progress: this.container.querySelector(".progress"), + progressHandle: this.container.querySelector(".progress-handle"), + timeDisplay: this.container.querySelector(".time"), + duration: this.container.querySelector(".duration"), + volumeBtn: this.container.querySelector(".volume-btn"), + }; + this.elements.volumeBtn.innerHTML = ` + + `; + } + + initializeEvents() { + // Play/Pause + this.elements.playBtn.addEventListener("click", () => this.togglePlay()); + + // Timeline + this.elements.timeline.addEventListener("click", (e) => this.seek(e)); + this.elements.timeline.addEventListener("touchstart", (e) => this.seek(e)); + + // Volume + this.elements.volumeBtn.addEventListener("click", () => this.toggleMute()); + + // Audio events + this.audio.addEventListener("timeupdate", () => this.updateProgress()); + this.audio.addEventListener("ended", () => this.audioEnded()); + } + + togglePlay() { + if (this.isPlaying) { + this.audio.pause(); + this.elements.playIcon.classList.remove("pause"); + this.elements.playIcon.classList.add("play_arrow"); + } else { + this.audio.play(); + this.elements.playIcon.classList.remove("play_arrow"); + this.elements.playIcon.classList.add("pause"); + } + this.isPlaying = !this.isPlaying; + } + + seek(e) { + const rect = this.elements.timeline.getBoundingClientRect(); + const pos = + (e.type.includes("touch") ? e.touches[0].clientX : e.clientX) - rect.left; + const percentage = pos / rect.width; + this.audio.currentTime = percentage * this.audio.duration; + } + + updateProgress() { + const percentage = (this.audio.currentTime / this.audio.duration) * 100; + this.elements.progress.style.width = `${percentage}%`; + this.elements.progressHandle.style.left = `${percentage}%`; + this.elements.timeDisplay.textContent = this.formatTime( + this.audio.currentTime, + ); + } + + formatTime(seconds) { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, "0")}`; + } + + toggleMute() { + this.audio.muted = !this.audio.muted; + if (this.audio.muted) { + this.elements.volumeBtn.innerHTML = + ''; + } else { + this.elements.volumeBtn.innerHTML = + ''; + } + } + + audioEnded() { + this.isPlaying = false; + this.elements.playIcon.classList.remove("pause"); + this.elements.playIcon.classList.add("play_arrow"); + } + + loadTrack(src) { + this.audio.src = src; + this.audio.load(); + } + + cleanup() { + this.audio.pause(); + this.audio.currentTime = 0; + this.isPlaying = false; + + this.elements.playBtn.removeEventListener("click", () => this.togglePlay()); + this.elements.timeline.removeEventListener("click", (e) => this.seek(e)); + this.elements.timeline.removeEventListener("touchstart", (e) => + this.seek(e), + ); + this.elements.volumeBtn.removeEventListener("click", () => + this.toggleMute(), + ); + this.audio.removeEventListener("timeupdate", () => this.updateProgress()); + this.audio.removeEventListener("ended", () => this.audioEnded()); + + const audioSrc = this.audio.src; + this.audio.src = ""; + this.audio.load(); + if (audioSrc.startsWith("blob:")) { + URL.revokeObjectURL(audioSrc); + } + } +} diff --git a/src/components/audioPlayer/style.scss b/src/components/audioPlayer/style.scss new file mode 100644 index 000000000..18f71d72c --- /dev/null +++ b/src/components/audioPlayer/style.scss @@ -0,0 +1,92 @@ +.audio-player { + background: var(--primary-color, #1e1e1e); + border-radius: 10px; + padding: 15px; + display: flex; + align-items: center; + gap: 15px; + width: 100%; + max-width: 400px; + user-select: none; + + .play-btn { + background: transparent; + border: none; + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s; + + span { + font-size: 20px; + color: var(--primary-text-color); + } + + &:hover { + background: color-mix(in srgb, var(--secondary-color) 30%, transparent); + } + } + + .timeline { + flex: 1; + height: 4px; + background: color-mix(in srgb, var(--secondary-color) 60%, transparent); + border-radius: 2px; + position: relative; + cursor: pointer; + + &:hover .progress-handle { + opacity: 1; + } + } + + .progress { + background: var(--primary-text-color, #fff); + width: 0%; + height: 100%; + border-radius: 2px; + transition: width 0.1s linear; + } + + .progress-handle { + width: 12px; + height: 12px; + background: var(--primary-text-color, #fff); + border-radius: 50%; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + pointer-events: none; + opacity: 0; + transition: opacity 0.2s; + } + + .time { + color: var(--primary-text-color, #fff); + font-family: monospace; + font-size: 12px; + min-width: 45px; + } + + .volume-control { + display: flex; + align-items: center; + gap: 8px; + } + + .volume-btn { + background: transparent; + border: none; + cursor: pointer; + + svg { + width: 20px; + height: 20px; + fill: var(--primary-text-color, #fff); + } + } +} diff --git a/src/lib/editorFile.js b/src/lib/editorFile.js index e3579085a..3113a6235 100644 --- a/src/lib/editorFile.js +++ b/src/lib/editorFile.js @@ -38,6 +38,17 @@ const { Range } = ace.require("ace/range"); */ export default class EditorFile { + /** + * Type of content this file represents but use page in case of custom pages etc + */ + #type = "editor"; + #tabIcon = "file file_type_default"; + /** + * Custom content element + * @type {HTMLElement} + */ + #content = null; + /** * If editor was focused before resize */ @@ -180,6 +191,23 @@ export default class EditorFile { this.#id = constants.DEFAULT_FILE_SESSION; } + if (options?.type) { + this.#type = options.type; + if (this.#type !== "editor") { + const container = ( +
+
{options.content}
+
+ ); + this.#content = container; + } else { + this.#content = options.content; + } + if (options.tabIcon) { + this.#tabIcon = options.tabIcon; + } + } + this.#uri = options?.uri; if (this.#id) doesExists = getFile(this.#id, "id"); @@ -194,6 +222,11 @@ export default class EditorFile { this.#tab = tile({ text: this.#name, + ...(this.#type !== "editor" && { + lead: ( + + ), + }), tail: tag("span", { className: "icon cancel", dataset: { @@ -249,13 +282,28 @@ export default class EditorFile { addFile(this); editorManager.emit("new-file", this); - this.session = ace.createEditSession(options?.text || ""); - this.setMode(); - this.#setupSession(); + + if (this.#type === "editor") { + this.session = ace.createEditSession(options?.text || ""); + this.setMode(); + this.#setupSession(); + } if (options?.render ?? true) this.render(); } + get type() { + return this.#type; + } + + get tabIcon() { + return this.#tabIcon; + } + + get content() { + return this.#content; + } + /** * File unique id. */ @@ -394,6 +442,7 @@ export default class EditorFile { * @param {'windows'|'unit'} value */ set eol(value) { + if (this.type !== "editor") return; if (this.eol === value) return; let text = this.session.getValue(); @@ -454,6 +503,9 @@ export default class EditorFile { * File icon */ get icon() { + if (this.#type !== "editor") { + return this.#tabIcon; + } return helpers.getIconForFile(this.filename); } @@ -483,6 +535,7 @@ export default class EditorFile { } async isChanged() { + if (this.type !== "editor") return false; // if file is not loaded or is loading then it is not changed. if (!this.loaded || this.loading) { return false; @@ -617,6 +670,7 @@ export default class EditorFile { * @returns {Promise} true if file is saved, false if not. */ save() { + if (this.type !== "editor") return Promise.resolve(false); return this.#save(false); } @@ -625,6 +679,7 @@ export default class EditorFile { * @returns {Promise} true if file is saved, false if not. */ saveAs() { + if (this.type !== "editor") return Promise.resolve(false); return this.#save(true); } @@ -633,6 +688,7 @@ export default class EditorFile { * @param {string} [mode] */ setMode(mode) { + if (this.type !== "editor") return; const modelist = ace.require("ace/ext/modelist"); const event = createFileEvent(this); this.#emit("changemode", event); @@ -667,19 +723,37 @@ export default class EditorFile { if (activeFile.id === this.id) return; activeFile.focusedBefore = activeFile.focused; activeFile.removeActive(); + + // Hide previous content if it exists + if (activeFile.type !== "editor" && activeFile.content) { + activeFile.content.style.display = "none"; + } } switchFile(this.id); - if (this.focused) { - editor.focus(); + // Show/hide appropriate content + if (this.type === "editor") { + editorManager.container.style.display = "block"; + if (this.focused) { + editor.focus(); + } else { + editor.blur(); + } } else { - editor.blur(); + editorManager.container.style.display = "none"; + if (this.content) { + this.content.style.display = "block"; + if (!this.content.parentElement) { + editorManager.container.parentElement.appendChild(this.content); + } + } } this.#tab.classList.add("active"); this.#tab.scrollIntoView(); - if (!this.loaded && !this.loading) { + + if (this.type === "editor" && !this.loaded && !this.loading) { this.#loadText(); } @@ -726,6 +800,18 @@ export default class EditorFile { ); defaultFile?.remove(); } + + // Show/hide editor based on content type + if (this.#type === "editor") { + editorManager.container.style.display = "block"; + if (this.#content) this.#content.style.display = "none"; + } else { + editorManager.container.style.display = "none"; + if (this.#content) { + this.#content.style.display = "block"; + editorManager.container.parentElement.appendChild(this.#content); + } + } } /** @@ -809,6 +895,7 @@ export default class EditorFile { } async #loadText() { + if (this.#type !== "editor") return; let value = ""; const { cursorPos, scrollLeft, scrollTop, folds, editable } = @@ -866,7 +953,7 @@ export default class EditorFile { if (Array.isArray(folds)) { const parsedFolds = EditorFile.#parseFolds(folds); - this.session.addFolds(parsedFolds); + this.session?.addFolds(parsedFolds); } }, 0); } catch (error) { @@ -897,24 +984,37 @@ export default class EditorFile { * @param {Array} folds */ static #parseFolds(folds) { - if (!Array.isArray(folds)) return; + if (this.type !== "editor") return []; + if (!Array.isArray(folds)) return []; + const foldDataAr = []; + folds.forEach((fold) => { + if (!fold || !fold.range) return; + const { range } = fold; const { start, end } = range; - const foldData = new Fold( - new Range(start.row, start.column, end.row, end.column), - fold.placeholder, - ); - if (fold.ranges.length > 0) { - const subFolds = parseFolds(fold.ranges); - foldData.subFolds = subFolds; - foldData.ranges = subFolds; - } + if (!start || !end) return; + + try { + const foldData = new Fold( + new Range(start.row, start.column, end.row, end.column), + fold.placeholder, + ); + + if (Array.isArray(fold.ranges) && fold.ranges.length > 0) { + const subFolds = EditorFile.#parseFolds(fold.ranges); + foldData.subFolds = subFolds; + foldData.ranges = subFolds; + } - foldDataAr.push(foldData); + foldDataAr.push(foldData); + } catch (error) { + console.warn("Error parsing fold:", error); + } }); + return foldDataAr; } @@ -945,6 +1045,7 @@ export default class EditorFile { * Setup Ace EditSession for the file */ #setupSession() { + if (this.type !== "editor") return; const { value: settings } = appSettings; this.session.setTabSize(settings.tabSize); @@ -963,13 +1064,18 @@ export default class EditorFile { #destroy() { this.#emit("close", createFileEvent(this)); appSettings.off("update:openFileListPos", this.#onFilePosChange); - this.session.off("changeScrollTop", EditorFile.#onscrolltop); - this.session.off("changeScrollLeft", EditorFile.#onscrollleft); - this.session.off("changeFold", EditorFile.#onfold); - this.#removeCache(); - this.session.destroy(); + if (this.type === "editor") { + this.session?.off("changeScrollTop", EditorFile.#onscrolltop); + this.session?.off("changeScrollLeft", EditorFile.#onscrollleft); + this.session?.off("changeFold", EditorFile.#onfold); + this.#removeCache(); + this.session?.destroy(); + delete this.session; + } else if (this.content) { + this.content.remove(); + } + this.#tab.remove(); - delete this.session; this.#tab = null; } diff --git a/src/lib/editorManager.js b/src/lib/editorManager.js index d91c5976a..f77a33039 100644 --- a/src/lib/editorManager.js +++ b/src/lib/editorManager.js @@ -573,7 +573,8 @@ async function EditorManager($header, $body) { */ function toggleProblemButton() { const fileWithProblems = manager.files.find((file) => { - const annotations = file.session.getAnnotations(); + if (file.type !== "editor") return false; + const annotations = file?.session?.getAnnotations(); return !!annotations.length; }); @@ -631,20 +632,37 @@ async function EditorManager($header, $body) { const file = manager.getFile(id); manager.activeFile?.tab.classList.remove("active"); + + // Hide previous content if it was non-editor + if (manager.activeFile?.type !== "editor" && manager.activeFile?.content) { + manager.activeFile.content.style.display = "none"; + } + manager.activeFile = file; - editor.setSession(file.session); - $header.text = file.filename; - $hScrollbar.hideImmediately(); - $vScrollbar.hideImmediately(); + if (file.type === "editor") { + editor.setSession(file.session); + editor.setReadOnly(!file.editable || !!file.loading); + $container.style.display = "block"; - setVScrollValue(); - if (!appSettings.value.textWrap) { - setHScrollValue(); - } + $hScrollbar.hideImmediately(); + $vScrollbar.hideImmediately(); - editor.setReadOnly(!file.editable || !!file.loading); + setVScrollValue(); + if (!appSettings.value.textWrap) { + setHScrollValue(); + } + } else { + $container.style.display = "none"; + if (file.content) { + file.content.style.display = "block"; + if (!file.content.parentElement) { + $container.parentElement.appendChild(file.content); + } + } + } + $header.text = file.filename; manager.onupdate("switch-file"); events.emit("switch-file", file); } diff --git a/src/lib/main.js b/src/lib/main.js index a7c724ce0..888a0e15b 100644 --- a/src/lib/main.js +++ b/src/lib/main.js @@ -566,6 +566,10 @@ function createFileMenu({ top, bottom, toggler }) { innerHTML: () => { const file = window.editorManager.activeFile; + if (file.type === "page") { + return ""; + } + if (file.loading) { $menu.classList.add("disabled"); } else { @@ -573,15 +577,18 @@ function createFileMenu({ top, bottom, toggler }) { } const { label: encoding } = getEncoding(file.encoding); - + const isEditorFile = file.type === "editor"; return mustache.render($_fileMenu, { ...strings, - file_mode: (file.session.getMode().$id || "").split("/").pop(), - file_encoding: encoding, + file_mode: isEditorFile + ? (file.session?.getMode()?.$id || "").split("/").pop() + : "", + file_encoding: isEditorFile ? encoding : "", file_read_only: !file.editable, file_on_disk: !!file.uri, - file_eol: file.eol, + file_eol: isEditorFile ? file.eol : "", copy_text: !!window.editorManager.editor.getCopyText(), + is_editor: isEditorFile, }); }, }); diff --git a/src/lib/openFile.js b/src/lib/openFile.js index 1df892f4f..dfd81b7e0 100644 --- a/src/lib/openFile.js +++ b/src/lib/openFile.js @@ -1,5 +1,5 @@ +import AudioPlayer from "components/audioPlayer"; import alert from "dialogs/alert"; -import box from "dialogs/box"; import confirm from "dialogs/confirm"; import loader from "dialogs/loader"; import fsOperation from "fileSystem"; @@ -60,6 +60,7 @@ export default async function openFile(file, options = {}) { if ( cursorPos && + existingCursorPos && existingCursorPos.row !== cursorPos.row && existingCursorPos.column !== cursorPos.column ) { @@ -101,34 +102,241 @@ export default async function openFile(file, options = {}) { return; } - const videoRegex = /\.(mp4|webm|ogg)$/i; - const imageRegex = /\.(jpe?g|png|gif|webp)$/i; - const audioRegex = /\.(mp3|wav|ogg)$/i; + const videoRegex = /\.(mp4|webm|ogg|mov|avi|wmv|flv|mkv|3gp)$/i; + const imageRegex = /\.(jpe?g|png|gif|webp|bmp|ico|avif|apng|tiff?)$/i; + const audioRegex = /\.(mp3|wav|ogg|m4a|aac|wma|flac|opus|3gp|mid|midi)$/i; if (videoRegex.test(name)) { const objectUrl = await fileToDataUrl(uri); - box( - name, - ``, + const videoContainer = ( +
); + + const videoEl = ( + + ); + + videoContainer.append(videoEl); + + new EditorFile(name, { + uri, + type: "video", + tabIcon: "file file_type_video", + content: videoContainer, + render: true, + }); return; } if (imageRegex.test(name)) { const objectUrl = await fileToDataUrl(uri); - box( - name, - ``, + const imageContainer = ( +
+ ); + + const imgEl = ( + ); + + let scale = 1; + let startX = 0; + let startY = 0; + let translateX = 0; + let translateY = 0; + let lastX = 0; + let lastY = 0; + + function getBoundaries() { + const containerRect = imageContainer.getBoundingClientRect(); + const imgRect = imgEl.getBoundingClientRect(); + + const maxX = + (imgRect.width * scale - containerRect.width) / (2 * scale); + const maxY = + (imgRect.height * scale - containerRect.height) / (2 * scale); + + return { + maxX: Math.max(0, maxX), + maxY: Math.max(0, maxY), + minX: -Math.max(0, maxX), + minY: -Math.max(0, maxY), + }; + } + + function constrainTranslation() { + const bounds = getBoundaries(); + translateX = Math.min(Math.max(translateX, bounds.minX), bounds.maxX); + translateY = Math.min(Math.max(translateY, bounds.minY), bounds.maxY); + } + + // Zoom with mouse wheel + imageContainer.addEventListener("wheel", (e) => { + e.preventDefault(); + const delta = e.deltaY > 0 ? -0.1 : 0.1; + const oldScale = scale; + scale = Math.max(0.1, Math.min(5, scale + delta)); + + // Adjust translation to zoom toward mouse position + const rect = imgEl.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const scaleChange = scale / oldScale; + translateX = mouseX - (mouseX - translateX) * scaleChange; + translateY = mouseY - (mouseY - translateY) * scaleChange; + + constrainTranslation(); + imgEl.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`; + }); + + // Pan image with mouse drag or touch + imageContainer.addEventListener("mousedown", startDrag); + imageContainer.addEventListener("touchstart", (e) => { + if (e.touches.length === 1) { + startDrag(e.touches[0]); + } else if (e.touches.length === 2) { + const touch1 = e.touches[0]; + const touch2 = e.touches[1]; + startX = Math.abs(touch1.clientX - touch2.clientX); + startY = Math.abs(touch1.clientY - touch2.clientY); + } + }); + + function startDrag(e) { + lastX = e.clientX; + lastY = e.clientY; + document.addEventListener("mousemove", onDrag); + document.addEventListener("mouseup", stopDrag); + document.addEventListener("touchmove", onTouchDrag); + document.addEventListener("touchend", stopDrag); + } + + function onDrag(e) { + const deltaX = e.clientX - lastX; + const deltaY = e.clientY - lastY; + translateX += deltaX / scale; + translateY += deltaY / scale; + lastX = e.clientX; + lastY = e.clientY; + constrainTranslation(); + imgEl.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`; + } + + function onTouchDrag(e) { + if (e.touches.length === 1) { + const touch = e.touches[0]; + const deltaX = touch.clientX - lastX; + const deltaY = touch.clientY - lastY; + translateX += deltaX / scale; + translateY += deltaY / scale; + lastX = touch.clientX; + lastY = touch.clientY; + constrainTranslation(); + imgEl.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`; + } else if (e.touches.length === 2) { + e.preventDefault(); + const touch1 = e.touches[0]; + const touch2 = e.touches[1]; + const currentX = Math.abs(touch1.clientX - touch2.clientX); + const currentY = Math.abs(touch1.clientY - touch2.clientY); + + const startDist = Math.sqrt(startX * startX + startY * startY); + const currentDist = Math.sqrt( + currentX * currentX + currentY * currentY, + ); + + const delta = (currentDist - startDist) / 100; + scale = Math.max(0.1, Math.min(5, scale + delta)); + constrainTranslation(); + imgEl.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`; + + startX = currentX; + startY = currentY; + } + } + + function stopDrag() { + document.removeEventListener("mousemove", onDrag); + document.removeEventListener("mouseup", stopDrag); + document.removeEventListener("touchmove", onTouchDrag); + document.removeEventListener("touchend", stopDrag); + } + + imageContainer.append(imgEl); + + new EditorFile(name, { + uri, + type: "image", + tabIcon: "file file_type_image", + content: imageContainer, + render: true, + }); return; } if (audioRegex.test(name)) { const objectUrl = await fileToDataUrl(uri); - box( - name, - ``, + const audioContainer = ( +
); + + const audioPlayer = new AudioPlayer(audioContainer); + audioPlayer.loadTrack(objectUrl); + + const audioTab = new EditorFile(name, { + uri, + type: "audio", + tabIcon: "file file_type_audio", + content: audioPlayer.container, + render: true, + }); + audioTab.onclose = () => { + audioPlayer.cleanup(); + }; return; } diff --git a/src/lib/saveState.js b/src/lib/saveState.js index 5bb4a78bf..68786378a 100644 --- a/src/lib/saveState.js +++ b/src/lib/saveState.js @@ -11,6 +11,7 @@ export default () => { const { value: settings } = appSettings; files.forEach((file) => { + if (file.type !== "editor") return; if (file.id === constants.DEFAULT_FILE_SESSION) return; const fileJson = { @@ -55,12 +56,18 @@ export default () => { }; function parseFolds(folds) { - return folds.map((fold) => { - const { range, ranges, placeholder } = fold; - return { - range, - ranges: parseFolds(ranges), - placeholder, - }; - }); + if (!Array.isArray(folds)) return []; + + return folds + .map((fold) => { + if (!fold || !fold.range) return null; + + const { range, ranges, placeholder } = fold; + return { + range, + ranges: parseFolds(ranges || []), + placeholder, + }; + }) + .filter(Boolean); } diff --git a/src/lib/showFileInfo.js b/src/lib/showFileInfo.js index 8ff60ad5c..39b3c79ec 100644 --- a/src/lib/showFileInfo.js +++ b/src/lib/showFileInfo.js @@ -17,24 +17,30 @@ export default async function showFileInfo(url) { try { const fs = fsOperation(url); const stats = await fs.stat(); - const value = await fs.readFile(settings.value.defaultFileEncoding); let { name, lastModified, length, type } = stats; length = filesize(length); lastModified = new Date(lastModified).toLocaleString(); const protocol = Url.getProtocol(url); + const fileType = type.toLowerCase(); const options = { name, lastModified, length, type, - lineCount: value.split(/\n+/).length, - wordCount: value.split(/\s+|\n+/).length, lang: strings, showUri: helpers.getVirtualPath(url), + isEditor: + fileType === "text/plain" || editorManager.activeFile.type === "editor", }; + if (editorManager.activeFile.type === "editor") { + const value = await fs.readFile(settings.value.defaultFileEncoding); + options.lineCount = value.split(/\n+/).length; + options.wordCount = value.split(/\s+|\n+/).length; + } + if (/s?ftp:/.test(protocol)) { options.shareUri = Url.join(CACHE_STORAGE, name); const fs = fsOperation(options.shareUri); diff --git a/src/pages/problems/problems.js b/src/pages/problems/problems.js index d9988c69a..df7156ffa 100644 --- a/src/pages/problems/problems.js +++ b/src/pages/problems/problems.js @@ -11,8 +11,9 @@ export default function Problems() { const $content =
; files.forEach((file) => { + if (file.type !== "editor") return; /**@type {[]} */ - const annotations = file.session.getAnnotations(); + const annotations = file.session?.getAnnotations(); if (!annotations.length) return; $content.append( diff --git a/src/plugins/sdcard/src/android/SDcard.java b/src/plugins/sdcard/src/android/SDcard.java index 12a5dc022..31de8d309 100644 --- a/src/plugins/sdcard/src/android/SDcard.java +++ b/src/plugins/sdcard/src/android/SDcard.java @@ -316,8 +316,12 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { if (uri == null) { activityResultCallback.error("No file selected"); } else { - takePermission(uri); - activityResultCallback.success(uri.toString()); + try { + takePermission(uri); + activityResultCallback.success(uri.toString()); + } catch (Exception e) { + activityResultCallback.error("Error taking permission: " + e.getMessage()); + } } activityResultCallback.success(uri.toString()); } diff --git a/src/styles/main.scss b/src/styles/main.scss index 1884d5160..fad7a5956 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -1,831 +1,848 @@ -@import './mixins.scss'; -@import './keyframes.scss'; -@import './fileInfo.scss'; -@import './markdown.scss'; - -:root { - --scrollbar-width: 4px; -} - -* { - margin: 0; - padding: 0; - - &:focus { - outline: none; - } -} - -html { - overflow: auto; -} - -html, -body { - width: 100%; - height: 100%; - font-size: 14px; -} - -body { - user-select: none; - font-family: 'Roboto', sans-serif; - -webkit-tap-highlight-color: transparent; - background-color: #9999ff; - background-color: var(--primary-color); - color: #252525; - color: var(--secondary-text-color); - - &.no-animation * { - animation: none !important; - transition: none !important; - box-shadow: none !important; - } - - &:not(.loading).title-loading { - &.title-loading-hide { - &::after { - background-image: none; - transform: translateX(-50%) translateY(-100%) scale3d(0.5, 0.5, 1); - opacity: 0; - animation: hide-loader 100ms ease-in 1; - } - } - - &::after { - content: ''; - background-color: #3333ff; - background-color: var(--primary-color); - border-radius: 50%; - position: fixed; - height: 40px; - width: 40px; - top: 6px; - left: 50%; - transform: translateX(-50%); - background-image: url(https://localhost/__cdvfile_assets__/www/res/tail-spin.svg); - background-repeat: no-repeat; - background-position: center; - background-size: 30px; - box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2); - box-shadow: 0 0 4px 0 var(--box-shadow-color); - border: solid 1px transparent; - border: solid 1px var(--popup-border-color); - animation: appear 100ms ease-out 1; - box-sizing: border-box; - z-index: 999; - } - } - - .main { - position: relative; - } -} - -a { - color: #615efd; - color: var(--link-text-color); -} - -.open-file-list { - position: relative; - height: 30px; - width: 100%; - background-color: #9999ff; - background-color: var(--primary-color); - overflow-x: auto !important; - overflow-y: hidden !important; - display: flex; - flex-direction: row !important; - color: white; - color: var(--primary-text-color); - z-index: 5; - - li.tile { - $width: 120px; - height: 100%; - overflow: hidden; - font-size: 0.8em; - align-items: center; - margin: 0; - padding: 0; - color: inherit; - min-width: $width; - min-width: var(--file-tab-width); - max-width: $width; - max-width: var(--file-tab-width); - - .text { - display: inline-block; - white-space: nowrap; - max-width: $width; - max-width: var(--file-tab-width); - overflow: hidden; - text-overflow: ellipsis; - margin: 0; - padding: 0; - color: inherit; - } - - &.notice { - &::before { - content: '•'; - color: #ffda0c; - font-size: 1.5em; - margin-left: 2.5px; - text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5); - } - } - - &.active { - border-top: solid 2px gold; - background-color: rgba(0, 0, 0, 0.2); - } - - .file, - .icon { - height: 24px; - width: 24px; - font-size: 1em; - background-size: 22px; - background-position: center; - color: inherit; - } - } -} - -a.icon { - pointer-events: all !important; - color: white; - - &:focus, - &:active { - border: none; - outline: none; - } -} - -.no-scroll { - &::-webkit-scrollbar { - width: 0px; - height: 0px; - } -} - -.list, -.prompt, -.scroll { - &::-webkit-scrollbar { - width: var(--scrollbar-width); - height: var(--scrollbar-width); - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background: rgba(0, 0, 0, 0.333); - background: var(--scrollbar-color); - border-radius: calc(var(--scrollbar-width) / 2); - } -} - -.icon { - user-select: none; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - text-decoration: none; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-position: center; - background-size: 24px; - background-repeat: no-repeat; - - &.hidden { - display: none !important; - } - - &.color { - display: flex; - - &::before { - content: ''; - height: 16px; - width: 16px; - border: solid 1px #a90000; - border: solid 1px var(--active-color); - background-color: currentColor; - color: inherit !important; - } - - &.dark { - color: #252525; - } - - &.light { - color: #ffffff; - } - } - - &.notice { - @include icon-badge; - } - - &.angularjs::before { - content: '\e92f'; - color: #dd0031; - } - - &.html::before { - content: '\e939'; - color: #e34f26; - } - - &.disabled { - opacity: 0.6; - pointer-events: none; - } - - &.dull { - opacity: 0.6; - } - - &:focus { - border: rgba(0, 0, 0, 0.1); - } - - &:not(.floating):active { - transition: all 100ms ease; - background-color: rgba(0, 0, 0, 0.2) !important; - background-color: var(--active-icon-color) !important; - } - - &.active { - background-color: rgba(0, 0, 0, 0.2) !important; - background-color: var(--active-icon-color) !important; - } - - &.foxdebug { - background-image: url(https://localhost/__cdvfile_assets__/www/res/logo/favicon.ico); - } - - &.no-icon { - max-width: 5px; - margin-right: 5px; - border-radius: 0; - } - - &.letters::before { - content: attr(data-letters); - text-transform: uppercase; - font-size: 0.6em; - font-weight: bolder; - } - - &.verified { - background-image: url(https://localhost/__cdvfile_assets__/www/res/verified.svg); - } -} - -.mask { - position: fixed; - left: 0; - top: 0; - display: block; - height: 100vh; - width: 100vw; - background-color: black; - opacity: 0; -} - -footer { - - &.button-container, - .button-container { - overflow-x: auto; - - .section { - max-width: 100%; - min-width: 100%; - - .icon.active { - @include active-icon; - } - } - - background-color: #9999ff; - background-color: var(--primary-color); - color: white; - color: var(--primary-text-color); - } -} - -.section, -.button-container { - display: flex; - min-height: 40px; - background-color: inherit; - color: inherit; - user-select: none; - width: 100%; - - &.primary { - button { - color: white !important; - color: var(--button-text-color) !important; - background-color: #39f !important; - background-color: var(--button-background-color) !important; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); - box-shadow: 0 0 4px var(--box-shadow-color); - border-radius: 4px; - - &:active { - background-color: #2c8ef0 !important; - background-color: var(--button-active-color) !important; - box-shadow: 0 0 2px rgba(0, 0, 0, 0.4); - box-shadow: inset 0 0 2px var(--box-shadow-color); - } - } - } - - &.disabled { - pointer-events: none; - - .icon, - input, - button { - opacity: 0.6; - } - } - - >button { - flex: 1; - display: inline-flex; - align-items: center; - justify-content: center; - border: none; - text-transform: uppercase; - background-color: inherit; - color: inherit; - - * { - pointer-events: none; - } - - &.disabled { - pointer-events: none; - opacity: 0.6; - } - - &:active { - transition: all 100ms ease; - box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.2); - box-shadow: inset 0 0 4px var(--box-shadow-color); - } - - &:disabled { - opacity: 0.6; - } - } - - textarea, - input { - flex: 2; - height: auto; - color: inherit; - border-bottom: 1px solid currentColor; - margin: 5px; - background-color: inherit; - - &::placeholder { - color: rgba(255, 255, 255, 0.6); - } - } - - .icon { - color: inherit; - font-size: 1.3em; - } - - .search, - .save { - font-size: 1em; - } -} - -input { - height: 40px; - outline: none; - border: none; - background-color: inherit; - border-bottom: solid 1px #252525; - border-bottom: solid 1px var(--secondary-text-color); - padding: 0; - box-sizing: border-box; - color: #252525; - color: var(--secondary-text-color); - caret-color: currentColor; - text-indent: 10px; - - &:focus { - border-bottom-color: #a90000 !important; - border-bottom-color: var(--active-color) !important; - } -} - -input, -textarea { - &::placeholder { - color: inherit; - opacity: 0.8; - } -} - -.search-status { - flex: 1; - display: flex; - color: white; - color: var(--primary-text-color); - align-items: center; - justify-content: center; - - span:not(:nth-child(2)) { - margin: 0 5px; - color: white; - color: var(--primary-text-color); - } -} - -.cursor { - position: absolute; - top: 0; - left: 0; - display: block; - border-radius: 50%; - background-color: white; - background-color: var(--primary-text-color); - border: solid 1px #666; - box-sizing: border-box; - transform-origin: left top; - z-index: 4; - pointer-events: none; - - &[data-size='60'] { - width: 60px; - height: 60px; - } - - &[data-size='30'] { - width: 30px; - height: 30px; - } - - &[data-size='20'] { - width: 20px; - height: 20px; - } - - &.end { - border-radius: 0% 50% 50% 50%; - } - - &.start { - border-radius: 50% 0 50% 50%; - } - - &.single { - transform: rotate(45deg); - border-radius: 0 50% 50% 50%; - } -} - -.cursor-menu { - position: absolute; - top: 0; - left: 0; - height: 40px; - background-color: #ffffff; - background-color: var(--secondary-color); - display: flex; - border-radius: 4px; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); - box-shadow: 0 0 8px var(--box-shadow-color); - border: none; - border: solid 1px var(--popup-border-color); - color: #252525; - color: var(--secondary-text-color); - transform-origin: left center; - z-index: 4; - - >span, - >div { - display: inline-flex; - align-items: center; - justify-content: center; - height: 100%; - font-size: 0.9em; - min-width: 50px; - color: inherit; - user-select: none; - white-space: nowrap; - - &.disabled { - opacity: 0.6; - pointer-events: none; - } - } -} - -.file { - display: flex; - align-items: center; - justify-content: center; - background-repeat: no-repeat; - background-position: 6px center; - background-size: 18px; - width: 30px; - height: 30px; -} - -.hr { - display: flex; - align-items: center; - margin: auto auto 15px auto; - - &::after, - &::before { - content: ''; - height: 1px; - width: 60px; - background-color: #252525; - background-color: var(--secondary-text-color); - margin: auto 15px; - opacity: 0.5; - } -} - -.d-none { - display: none !important; -} - -.floating.icon { - position: fixed; - height: 50px; - width: 50px; - font-size: 1.6rem; - border: 1px solid; - background-color: #9999ff; - background-color: var(--primary-color); - top: 10px; - right: 10px; - opacity: 0.2; - box-sizing: border-box; - color: white; - color: var(--primary-text-color); - transition: all 300ms ease; - box-shadow: -5px 5px 20px 0px rgba(0, 0, 0, 0.5); - - &:active { - transition: all 100ms ease; - box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.5); - } - - &.hide { - opacity: 0 !important; - } -} - -button { - &.floating.icon { - z-index: 1; - opacity: 1; - - &:disabled { - opacity: 0.2; - } - } -} - -#social-links { - position: relative; - height: 60px; - font-size: 1.2em; - width: 100%; - text-align: center; - - &::after { - display: block; - width: 100%; - content: attr(title); - text-align: center; - font-size: 0.5em; - text-transform: none; - } - - a { - display: inline-flex; - min-height: 40px; - min-width: 40px; - text-shadow: 0 0 1px white; - - &.github { - color: black; - } - } -} - -#header-toggler { - display: none; - top: 10px; - right: 10px; - z-index: 1; - height: 40px; - width: 40px; -} - -#sidebar-toggler { - display: none; - top: 10px; - left: 10px; - z-index: 1; - height: 40px; - width: 40px; -} - -#quicktools-toggler { - top: auto; - bottom: 10px; - right: 10px; - z-index: 1; -} - -.sake { - animation: sake 3s ease-out infinite; -} - -.flex-center { - display: flex; - align-items: center; - justify-content: center; -} - -.link { - text-decoration: underline; -} - -.w-resize { - cursor: w-resize; -} - -.note { - margin: 20px 0; - - .note-title { - background-color: rgba(0, 0, 0, 0.2); - display: flex; - align-items: center; - justify-content: center; - height: 30px; - text-transform: uppercase; - - .icon { - margin: 0 10px; - } - } - - p { - padding: 10px; - box-sizing: border-box; - opacity: 0.8; - font-size: 0.9rem; - } -} - -input[type="search"]::-webkit-search-decoration, -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-results-button, -input[type="search"]::-webkit-search-results-decoration { - -webkit-appearance: none; -} - -.notification-toast-container { - position: absolute; - bottom: 20px; - right: 20px; - display: flex; - flex-direction: column; - gap: 8px; - z-index: 1000; - - .notification-toast { - padding: 12px; - border-radius: 6px; - background: var(--secondary-color); - min-width: 300px; - max-width: 400px; - display: flex; - gap: 12px; - align-items: flex-start; - box-shadow: 0 4px 12px var(--box-shadow-color); - animation: toastSlideIn 0.3s ease-out; - transition: all 0.3s ease; - border: 1px solid var(--border-color); - word-break: break-word; - white-space: normal; - - &.hiding { - transform: translateX(120%); - opacity: 0; - } - - .close-icon { - cursor: pointer; - font-size: 14px; - color: var(--secondary-text-color); - margin-left: auto; - - &:hover { - color: var(--button-background-color); - } - } - - .notification-icon { - width: 20px; - height: 20px; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - color: var(--primary-text-color); - } - - .notification-content { - flex: 1; - min-width: 0; - - .notification-title { - font-size: 13px; - font-weight: 500; - margin-bottom: 4px; - color: var(--primary-text-color); - display: flex; - justify-content: space-between; - align-items: center; - } - - .notification-message { - font-size: 12px; - color: var(--secondary-text-color); - line-height: 1.4; - overflow-wrap: break-word; - hyphens: auto; - } - } - - &.success { - .notification-icon { - color: #48c158; - } - } - - &.warning { - .notification-icon { - color: var(--danger-text-color); - } - } - - &.error { - .notification-icon { - color: var(--error-text-color); - } - } - - &.info { - .notification-icon { - color: var(--primary-text-color); - } - } - } - - @media (max-width: 768px) { - .notification-toast { - min-width: auto; - max-width: calc(100vw - 40px); - } - } -} - -@keyframes toastSlideIn { - from { - transform: translateX(120%); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} +@import "./mixins.scss"; +@import "./keyframes.scss"; +@import "./fileInfo.scss"; +@import "./markdown.scss"; + +:root { + --scrollbar-width: 4px; +} + +* { + margin: 0; + padding: 0; + + &:focus { + outline: none; + } +} + +html { + overflow: auto; +} + +html, +body { + width: 100%; + height: 100%; + font-size: 14px; +} + +body { + user-select: none; + font-family: "Roboto", sans-serif; + -webkit-tap-highlight-color: transparent; + background-color: #9999ff; + background-color: var(--primary-color); + color: #252525; + color: var(--secondary-text-color); + + &.no-animation * { + animation: none !important; + transition: none !important; + box-shadow: none !important; + } + + &:not(.loading).title-loading { + &.title-loading-hide { + &::after { + background-image: none; + transform: translateX(-50%) translateY(-100%) scale3d(0.5, 0.5, 1); + opacity: 0; + animation: hide-loader 100ms ease-in 1; + } + } + + &::after { + content: ""; + background-color: #3333ff; + background-color: var(--primary-color); + border-radius: 50%; + position: fixed; + height: 40px; + width: 40px; + top: 6px; + left: 50%; + transform: translateX(-50%); + background-image: url(https://localhost/__cdvfile_assets__/www/res/tail-spin.svg); + background-repeat: no-repeat; + background-position: center; + background-size: 30px; + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2); + box-shadow: 0 0 4px 0 var(--box-shadow-color); + border: solid 1px transparent; + border: solid 1px var(--popup-border-color); + animation: appear 100ms ease-out 1; + box-sizing: border-box; + z-index: 999; + } + } + + .main { + position: relative; + } +} + +a { + color: #615efd; + color: var(--link-text-color); +} + +.open-file-list { + position: relative; + height: 30px; + width: 100%; + background-color: #9999ff; + background-color: var(--primary-color); + overflow-x: auto !important; + overflow-y: hidden !important; + display: flex; + flex-direction: row !important; + color: white; + color: var(--primary-text-color); + z-index: 5; + + li.tile { + $width: 120px; + height: 100%; + overflow: hidden; + font-size: 0.8em; + align-items: center; + margin: 0; + padding: 0; + color: inherit; + min-width: $width; + min-width: var(--file-tab-width); + max-width: $width; + max-width: var(--file-tab-width); + + .text { + display: inline-block; + white-space: nowrap; + max-width: $width; + max-width: var(--file-tab-width); + overflow: hidden; + text-overflow: ellipsis; + margin: 0; + padding: 0; + color: inherit; + } + + &.notice { + &::before { + content: "•"; + color: #ffda0c; + font-size: 1.5em; + margin-left: 2.5px; + text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.5); + } + } + + &.active { + border-top: solid 2px gold; + background-color: rgba(0, 0, 0, 0.2); + } + + .file, + .icon { + height: 24px; + width: 24px; + font-size: 1em; + background-size: 22px; + background-position: center; + color: inherit; + } + } +} + +a.icon { + pointer-events: all !important; + color: white; + + &:focus, + &:active { + border: none; + outline: none; + } +} + +.no-scroll { + &::-webkit-scrollbar { + width: 0px; + height: 0px; + } +} + +.list, +.prompt, +.scroll { + &::-webkit-scrollbar { + width: var(--scrollbar-width); + height: var(--scrollbar-width); + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.333); + background: var(--scrollbar-color); + border-radius: calc(var(--scrollbar-width) / 2); + } +} + +.icon { + user-select: none; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + text-decoration: none; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-position: center; + background-size: 24px; + background-repeat: no-repeat; + + &.hidden { + display: none !important; + } + + &.color { + display: flex; + + &::before { + content: ""; + height: 16px; + width: 16px; + border: solid 1px #a90000; + border: solid 1px var(--active-color); + background-color: currentColor; + color: inherit !important; + } + + &.dark { + color: #252525; + } + + &.light { + color: #ffffff; + } + } + + &.notice { + @include icon-badge; + } + + &.angularjs::before { + content: "\e92f"; + color: #dd0031; + } + + &.html::before { + content: "\e939"; + color: #e34f26; + } + + &.disabled { + opacity: 0.6; + pointer-events: none; + } + + &.dull { + opacity: 0.6; + } + + &:focus { + border: rgba(0, 0, 0, 0.1); + } + + &:not(.floating):active { + transition: all 100ms ease; + background-color: rgba(0, 0, 0, 0.2) !important; + background-color: var(--active-icon-color) !important; + } + + &.active { + background-color: rgba(0, 0, 0, 0.2) !important; + background-color: var(--active-icon-color) !important; + } + + &.foxdebug { + background-image: url(https://localhost/__cdvfile_assets__/www/res/logo/favicon.ico); + } + + &.no-icon { + max-width: 5px; + margin-right: 5px; + border-radius: 0; + } + + &.letters::before { + content: attr(data-letters); + text-transform: uppercase; + font-size: 0.6em; + font-weight: bolder; + } + + &.verified { + background-image: url(https://localhost/__cdvfile_assets__/www/res/verified.svg); + } +} + +.mask { + position: fixed; + left: 0; + top: 0; + display: block; + height: 100vh; + width: 100vw; + background-color: black; + opacity: 0; +} + +footer { + &.button-container, + .button-container { + overflow-x: auto; + + .section { + max-width: 100%; + min-width: 100%; + + .icon.active { + @include active-icon; + } + } + + background-color: #9999ff; + background-color: var(--primary-color); + color: white; + color: var(--primary-text-color); + } +} + +.section, +.button-container { + display: flex; + min-height: 40px; + background-color: inherit; + color: inherit; + user-select: none; + width: 100%; + + &.primary { + button { + color: white !important; + color: var(--button-text-color) !important; + background-color: #39f !important; + background-color: var(--button-background-color) !important; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); + box-shadow: 0 0 4px var(--box-shadow-color); + border-radius: 4px; + + &:active { + background-color: #2c8ef0 !important; + background-color: var(--button-active-color) !important; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.4); + box-shadow: inset 0 0 2px var(--box-shadow-color); + } + } + } + + &.disabled { + pointer-events: none; + + .icon, + input, + button { + opacity: 0.6; + } + } + + > button { + flex: 1; + display: inline-flex; + align-items: center; + justify-content: center; + border: none; + text-transform: uppercase; + background-color: inherit; + color: inherit; + + * { + pointer-events: none; + } + + &.disabled { + pointer-events: none; + opacity: 0.6; + } + + &:active { + transition: all 100ms ease; + box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 0 4px var(--box-shadow-color); + } + + &:disabled { + opacity: 0.6; + } + } + + textarea, + input { + flex: 2; + height: auto; + color: inherit; + border-bottom: 1px solid currentColor; + margin: 5px; + background-color: inherit; + + &::placeholder { + color: rgba(255, 255, 255, 0.6); + } + } + + .icon { + color: inherit; + font-size: 1.3em; + } + + .search, + .save { + font-size: 1em; + } +} + +input { + height: 40px; + outline: none; + border: none; + background-color: inherit; + border-bottom: solid 1px #252525; + border-bottom: solid 1px var(--secondary-text-color); + padding: 0; + box-sizing: border-box; + color: #252525; + color: var(--secondary-text-color); + caret-color: currentColor; + text-indent: 10px; + + &:focus { + border-bottom-color: #a90000 !important; + border-bottom-color: var(--active-color) !important; + } +} + +input, +textarea { + &::placeholder { + color: inherit; + opacity: 0.8; + } +} + +.search-status { + flex: 1; + display: flex; + color: white; + color: var(--primary-text-color); + align-items: center; + justify-content: center; + + span:not(:nth-child(2)) { + margin: 0 5px; + color: white; + color: var(--primary-text-color); + } +} + +.cursor { + position: absolute; + top: 0; + left: 0; + display: block; + border-radius: 50%; + background-color: white; + background-color: var(--primary-text-color); + border: solid 1px #666; + box-sizing: border-box; + transform-origin: left top; + z-index: 4; + pointer-events: none; + + &[data-size="60"] { + width: 60px; + height: 60px; + } + + &[data-size="30"] { + width: 30px; + height: 30px; + } + + &[data-size="20"] { + width: 20px; + height: 20px; + } + + &.end { + border-radius: 0% 50% 50% 50%; + } + + &.start { + border-radius: 50% 0 50% 50%; + } + + &.single { + transform: rotate(45deg); + border-radius: 0 50% 50% 50%; + } +} + +.cursor-menu { + position: absolute; + top: 0; + left: 0; + height: 40px; + background-color: #ffffff; + background-color: var(--secondary-color); + display: flex; + border-radius: 4px; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); + box-shadow: 0 0 8px var(--box-shadow-color); + border: none; + border: solid 1px var(--popup-border-color); + color: #252525; + color: var(--secondary-text-color); + transform-origin: left center; + z-index: 4; + + > span, + > div { + display: inline-flex; + align-items: center; + justify-content: center; + height: 100%; + font-size: 0.9em; + min-width: 50px; + color: inherit; + user-select: none; + white-space: nowrap; + + &.disabled { + opacity: 0.6; + pointer-events: none; + } + } +} + +.file { + display: flex; + align-items: center; + justify-content: center; + background-repeat: no-repeat; + background-position: 6px center; + background-size: 18px; + width: 30px; + height: 30px; +} + +.hr { + display: flex; + align-items: center; + margin: auto auto 15px auto; + + &::after, + &::before { + content: ""; + height: 1px; + width: 60px; + background-color: #252525; + background-color: var(--secondary-text-color); + margin: auto 15px; + opacity: 0.5; + } +} + +.d-none { + display: none !important; +} + +.floating.icon { + position: fixed; + height: 50px; + width: 50px; + font-size: 1.6rem; + border: 1px solid; + background-color: #9999ff; + background-color: var(--primary-color); + top: 10px; + right: 10px; + opacity: 0.2; + box-sizing: border-box; + color: white; + color: var(--primary-text-color); + transition: all 300ms ease; + box-shadow: -5px 5px 20px 0px rgba(0, 0, 0, 0.5); + + &:active { + transition: all 100ms ease; + box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.5); + } + + &.hide { + opacity: 0 !important; + } +} + +button { + &.floating.icon { + z-index: 1; + opacity: 1; + + &:disabled { + opacity: 0.2; + } + } +} + +#social-links { + position: relative; + height: 60px; + font-size: 1.2em; + width: 100%; + text-align: center; + + &::after { + display: block; + width: 100%; + content: attr(title); + text-align: center; + font-size: 0.5em; + text-transform: none; + } + + a { + display: inline-flex; + min-height: 40px; + min-width: 40px; + text-shadow: 0 0 1px white; + + &.github { + color: black; + } + } +} + +#header-toggler { + display: none; + top: 10px; + right: 10px; + z-index: 1; + height: 40px; + width: 40px; +} + +#sidebar-toggler { + display: none; + top: 10px; + left: 10px; + z-index: 1; + height: 40px; + width: 40px; +} + +#quicktools-toggler { + top: auto; + bottom: 10px; + right: 10px; + z-index: 1; +} + +.sake { + animation: sake 3s ease-out infinite; +} + +.flex-center { + display: flex; + align-items: center; + justify-content: center; +} + +.link { + text-decoration: underline; +} + +.w-resize { + cursor: w-resize; +} + +.note { + margin: 20px 0; + + .note-title { + background-color: rgba(0, 0, 0, 0.2); + display: flex; + align-items: center; + justify-content: center; + height: 30px; + text-transform: uppercase; + + .icon { + margin: 0 10px; + } + } + + p { + padding: 10px; + box-sizing: border-box; + opacity: 0.8; + font-size: 0.9rem; + } +} + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-results-button, +input[type="search"]::-webkit-search-results-decoration { + -webkit-appearance: none; +} + +.tab-page-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: hidden; +} + +.tab-page-content { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow: auto; +} + +.notification-toast-container { + position: absolute; + bottom: 20px; + right: 20px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 1000; + + .notification-toast { + padding: 12px; + border-radius: 6px; + background: var(--secondary-color); + min-width: 300px; + max-width: 400px; + display: flex; + gap: 12px; + align-items: flex-start; + box-shadow: 0 4px 12px var(--box-shadow-color); + animation: toastSlideIn 0.3s ease-out; + transition: all 0.3s ease; + border: 1px solid var(--border-color); + word-break: break-word; + white-space: normal; + + &.hiding { + transform: translateX(120%); + opacity: 0; + } + + .close-icon { + cursor: pointer; + font-size: 14px; + color: var(--secondary-text-color); + margin-left: auto; + + &:hover { + color: var(--button-background-color); + } + } + + .notification-icon { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--primary-text-color); + } + + .notification-content { + flex: 1; + min-width: 0; + + .notification-title { + font-size: 13px; + font-weight: 500; + margin-bottom: 4px; + color: var(--primary-text-color); + display: flex; + justify-content: space-between; + align-items: center; + } + + .notification-message { + font-size: 12px; + color: var(--secondary-text-color); + line-height: 1.4; + overflow-wrap: break-word; + hyphens: auto; + } + } + + &.success { + .notification-icon { + color: #48c158; + } + } + + &.warning { + .notification-icon { + color: var(--danger-text-color); + } + } + + &.error { + .notification-icon { + color: var(--error-text-color); + } + } + + &.info { + .notification-icon { + color: var(--primary-text-color); + } + } + } + + @media (max-width: 768px) { + .notification-toast { + min-width: auto; + max-width: calc(100vw - 40px); + } + } +} + +@keyframes toastSlideIn { + from { + transform: translateX(120%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} diff --git a/src/views/file-info.hbs b/src/views/file-info.hbs index e30cc62d1..c8ddc377f 100644 --- a/src/views/file-info.hbs +++ b/src/views/file-info.hbs @@ -10,14 +10,16 @@ {{lang.last modified}} {{lastModified}} -
- {{lang.line count}} - {{lineCount}} -
-
- {{lang.word count}} - {{wordCount}} -
+{{#isEditor}} +
+ {{lang.line count}} + {{lineCount}} +
+
+ {{lang.word count}} + {{wordCount}} +
+{{/isEditor}}
{{lang.type}} {{type}} @@ -26,4 +28,4 @@ {{lang.path}} {{showUri}}
- \ No newline at end of file + diff --git a/src/views/file-menu.hbs b/src/views/file-menu.hbs index ee32817b3..734e2a5cb 100644 --- a/src/views/file-menu.hbs +++ b/src/views/file-menu.hbs @@ -6,6 +6,7 @@
  • {{rename}}
  • +{{#is_editor}}
  • {{syntax highlighting}} @@ -24,6 +25,9 @@ {{file_eol}}
  • +{{/is_editor}} + +{{#is_editor}}
  • {{read only}} @@ -38,6 +42,7 @@ {{format}}
  • +{{/is_editor}}
    {{#file_on_disk}}
  • @@ -53,6 +58,8 @@
  • {{/file_on_disk}} + +{{#is_editor}}
  • {{search}} @@ -93,4 +100,5 @@
    {{select all}}
    -
  • \ No newline at end of file + +{{/is_editor}}