1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-02 19:48:27 -04:00
Files
DankMaterialShell/quickshell/Services/NotepadStorageService.qml
T

497 lines
14 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import QtCore
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
Singleton {
id: root
readonly property var log: Log.scoped("NotepadStorageService")
property int refCount: 0
readonly property string baseDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericStateLocation) + "/DankMaterialShell")
readonly property string filesDir: baseDir + "/notepad-files"
readonly property string metadataPath: baseDir + "/notepad-session.json"
property var tabs: []
property int currentTabIndex: 0
property var tabsBeingCreated: ({})
property bool metadataLoaded: false
// Shared live edit state across slideout and popout surfaces.
property var sessionBuffers: ({})
property int sessionBufferRevision: 0
function setSessionBuffer(tabId, content, baseline) {
if (tabId === undefined || tabId === null || tabId < 0)
return;
var next = Object.assign({}, sessionBuffers);
if (content !== baseline) {
next[tabId] = {
content: content,
baseline: baseline
};
} else {
delete next[tabId];
}
sessionBuffers = next;
sessionBufferRevision++;
}
function getSessionBuffer(tabId) {
return sessionBuffers[tabId];
}
function clearSessionBuffer(tabId) {
if (sessionBuffers[tabId] === undefined)
return;
var next = Object.assign({}, sessionBuffers);
delete next[tabId];
sessionBuffers = next;
sessionBufferRevision++;
}
property var conflictTabId: -1
property string conflictDiskContent: ""
function flagConflict(tabId, diskContent) {
conflictDiskContent = diskContent;
conflictTabId = tabId;
}
function clearConflict() {
conflictTabId = -1;
conflictDiskContent = "";
}
Component.onCompleted: {
ensureDirectories();
}
FileView {
id: metadataFile
path: root.refCount > 0 ? root.metadataPath : ""
blockWrites: true
atomicWrites: true
onLoaded: {
try {
var data = JSON.parse(text());
root.tabs = data.tabs || [];
root.currentTabIndex = data.currentTabIndex || 0;
root.metadataLoaded = true;
root.validateTabs();
} catch (e) {
log.warn("Failed to parse notepad metadata:", e);
root.createDefaultTab();
}
}
onLoadFailed: {
root.createDefaultTab();
}
}
onRefCountChanged: {
if (refCount === 1 && !metadataLoaded) {
metadataFile.path = "";
metadataFile.path = root.metadataPath;
}
}
function ensureDirectories() {
Proc.runCommand("", ["mkdir", "-p", root.baseDir, root.filesDir], null);
}
function loadMetadata() {
metadataFile.path = "";
metadataFile.path = root.metadataPath;
}
function createDefaultTab() {
var id = Date.now();
var filePath = "notepad-files/untitled-" + id + ".txt";
var fullPath = baseDir + "/" + filePath;
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
newTabsBeingCreated[id] = true;
tabsBeingCreated = newTabsBeingCreated;
root.createEmptyFile(fullPath, function () {
root.tabs = [
{
id: id,
title: I18n.tr("Untitled"),
filePath: filePath,
isTemporary: true,
lastModified: new Date().toISOString(),
cursorPosition: 0,
scrollPosition: 0
}
];
root.currentTabIndex = 0;
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
delete updatedTabsBeingCreated[id];
tabsBeingCreated = updatedTabsBeingCreated;
root.saveMetadata();
});
}
function saveMetadata() {
var metadata = {
version: 1,
currentTabIndex: currentTabIndex,
tabs: tabs
};
metadataFile.setText(JSON.stringify(metadata, null, 2));
}
function getTabById(tabId) {
for (var i = 0; i < tabs.length; i++) {
if (tabs[i].id === tabId)
return tabs[i];
}
return null;
}
function loadTabContent(tabIndex, callback) {
if (tabIndex < 0 || tabIndex >= tabs.length) {
callback("");
return;
}
var tab = tabs[tabIndex];
var requestTabId = tab.id;
var fullPath = tab.isTemporary ? baseDir + "/" + tab.filePath : tab.filePath;
if (tabsBeingCreated[tab.id]) {
Qt.callLater(() => {
loadTabContent(tabIndex, callback);
});
return;
}
Proc.runCommand("", ["test", "-f", fullPath], (output, exitCode) => {
var currentTab = root.getTabById(requestTabId);
var currentPath = currentTab ? (currentTab.isTemporary ? baseDir + "/" + currentTab.filePath : currentTab.filePath) : "";
if (!currentTab || currentPath !== fullPath) {
callback("");
return;
}
if (exitCode === 0) {
tabFileLoaderComponent.createObject(root, {
path: fullPath,
callback: callback
});
} else {
log.warn("Tab file does not exist:", fullPath);
callback("");
}
});
}
function saveTabContent(tabIndex, content) {
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
var tab = tabs[tabIndex];
var fullPath = tab.isTemporary ? baseDir + "/" + tab.filePath : tab.filePath;
var saver = tabFileSaverComponent.createObject(root, {
path: fullPath,
content: content,
tabIndex: tabIndex
});
}
function createNewTab() {
var id = Date.now();
var filePath = "notepad-files/untitled-" + id + ".txt";
var fullPath = baseDir + "/" + filePath;
var newTab = {
id: id,
title: I18n.tr("Untitled"),
filePath: filePath,
isTemporary: true,
lastModified: new Date().toISOString(),
cursorPosition: 0,
scrollPosition: 0
};
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
newTabsBeingCreated[id] = true;
tabsBeingCreated = newTabsBeingCreated;
createEmptyFile(fullPath, function () {
var newTabs = tabs.slice();
newTabs.push(newTab);
tabs = newTabs;
currentTabIndex = tabs.length - 1;
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
delete updatedTabsBeingCreated[id];
tabsBeingCreated = updatedTabsBeingCreated;
saveMetadata();
});
return newTab;
}
function createTabForFile(path) {
var id = Date.now();
var fileName = path.split('/').pop();
var newTab = {
id: id,
title: fileName,
filePath: path,
isTemporary: false,
lastModified: new Date().toISOString(),
cursorPosition: 0,
scrollPosition: 0
};
var newTabs = tabs.slice();
newTabs.push(newTab);
tabs = newTabs;
currentTabIndex = tabs.length - 1;
saveMetadata();
return newTab;
}
function closeTab(tabIndex) {
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
var newTabs = tabs.slice();
var closedTabId = newTabs[tabIndex] ? newTabs[tabIndex].id : -1;
clearSessionBuffer(closedTabId);
if (conflictTabId === closedTabId)
clearConflict();
if (newTabs.length <= 1) {
var id = Date.now();
var filePath = "notepad-files/untitled-" + id + ".txt";
var newTabsBeingCreated = Object.assign({}, tabsBeingCreated);
newTabsBeingCreated[id] = true;
tabsBeingCreated = newTabsBeingCreated;
createEmptyFile(baseDir + "/" + filePath, function () {
newTabs[0] = {
id: id,
title: I18n.tr("Untitled"),
filePath: filePath,
isTemporary: true,
lastModified: new Date().toISOString(),
cursorPosition: 0,
scrollPosition: 0
};
currentTabIndex = 0;
tabs = newTabs;
var updatedTabsBeingCreated = Object.assign({}, tabsBeingCreated);
delete updatedTabsBeingCreated[id];
tabsBeingCreated = updatedTabsBeingCreated;
saveMetadata();
});
return;
} else {
var tabToDelete = newTabs[tabIndex];
if (tabToDelete && tabToDelete.isTemporary) {
deleteFile(baseDir + "/" + tabToDelete.filePath);
}
newTabs.splice(tabIndex, 1);
if (currentTabIndex >= newTabs.length) {
currentTabIndex = newTabs.length - 1;
} else if (currentTabIndex > tabIndex) {
currentTabIndex -= 1;
}
}
tabs = newTabs;
saveMetadata();
}
function switchToTab(tabIndex) {
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
currentTabIndex = tabIndex;
saveMetadata();
}
function reorderTab(fromIndex, toIndex) {
if (fromIndex < 0 || fromIndex >= tabs.length || toIndex < 0 || toIndex >= tabs.length)
return;
if (fromIndex === toIndex)
return;
var newTabs = tabs.slice();
var moved = newTabs.splice(fromIndex, 1)[0];
newTabs.splice(toIndex, 0, moved);
tabs = newTabs;
if (currentTabIndex === fromIndex) {
currentTabIndex = toIndex;
} else if (fromIndex < currentTabIndex && toIndex >= currentTabIndex) {
currentTabIndex--;
} else if (fromIndex > currentTabIndex && toIndex <= currentTabIndex) {
currentTabIndex++;
}
saveMetadata();
}
function saveTabAs(tabIndex, userPath) {
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
var tab = tabs[tabIndex];
var fileName = userPath.split('/').pop();
if (tab.isTemporary) {
var tempPath = baseDir + "/" + tab.filePath;
copyFile(tempPath, userPath);
deleteFile(tempPath);
}
var newTabs = tabs.slice();
newTabs[tabIndex] = Object.assign({}, tab, {
title: fileName,
filePath: userPath,
isTemporary: false,
lastModified: new Date().toISOString()
});
tabs = newTabs;
saveMetadata();
}
function renameTab(tabIndex, newTitle) {
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
var trimmed = (newTitle || "").trim();
var tab = tabs[tabIndex];
if (trimmed.length === 0 || trimmed === tab.title)
return;
if (tab.isTemporary) {
updateTabMetadata(tabIndex, {
title: trimmed
});
return;
}
var dir = tab.filePath.substring(0, tab.filePath.lastIndexOf('/') + 1);
var newPath = dir + trimmed;
moveFile(tab.filePath, newPath);
updateTabMetadata(tabIndex, {
title: trimmed,
filePath: newPath
});
}
function updateTabMetadata(tabIndex, properties) {
if (tabIndex < 0 || tabIndex >= tabs.length)
return;
var newTabs = tabs.slice();
var updatedTab = Object.assign({}, newTabs[tabIndex], properties);
updatedTab.lastModified = new Date().toISOString();
newTabs[tabIndex] = updatedTab;
tabs = newTabs;
saveMetadata();
}
function validateTabs() {
var validTabs = [];
for (var i = 0; i < tabs.length; i++) {
var tab = tabs[i];
validTabs.push(tab);
}
tabs = validTabs;
if (tabs.length === 0) {
root.createDefaultTab();
}
}
Component {
id: tabFileLoaderComponent
FileView {
property var callback
blockLoading: true
preload: true
onLoaded: {
callback(text());
destroy();
}
onLoadFailed: {
callback("");
destroy();
}
}
}
Component {
id: tabFileSaverComponent
FileView {
property string content
property int tabIndex
property var creationCallback
blockWrites: false
atomicWrites: true
Component.onCompleted: setText(content)
onSaved: {
if (tabIndex >= 0) {
root.updateTabMetadata(tabIndex, {});
}
if (creationCallback) {
creationCallback();
}
destroy();
}
onSaveFailed: {
log.error("Failed to save tab content");
if (creationCallback) {
creationCallback();
}
destroy();
}
}
}
function createEmptyFile(path, callback) {
var cleanPath = decodeURI(path.toString());
if (!cleanPath.startsWith("/")) {
cleanPath = baseDir + "/" + cleanPath;
}
Proc.runCommand("", ["touch", cleanPath], (output, exitCode) => {
if (callback)
callback();
});
}
function copyFile(source, destination) {
Proc.runCommand("", ["cp", source, destination], null);
}
function deleteFile(path) {
Proc.runCommand("", ["rm", "-f", path], null);
}
function moveFile(source, destination) {
Proc.runCommand("", ["mv", source, destination], null);
}
}