1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00
Files
DankMaterialShell/Services/NotepadStorageService.qml
2025-11-02 14:20:33 -05:00

432 lines
12 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import QtCore
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
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
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) {
console.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() {
mkdirProcess.running = true
}
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 loadTabContent(tabIndex, callback) {
if (tabIndex < 0 || tabIndex >= tabs.length) {
callback("")
return
}
var tab = tabs[tabIndex]
var fullPath = tab.isTemporary
? baseDir + "/" + tab.filePath
: tab.filePath
if (tabsBeingCreated[tab.id]) {
Qt.callLater(() => {
loadTabContent(tabIndex, callback)
})
return
}
var fileChecker = fileExistsComponent.createObject(root, {
path: fullPath,
callback: (exists) => {
if (exists) {
var loader = tabFileLoaderComponent.createObject(root, {
path: fullPath,
callback: callback
})
} else {
console.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 closeTab(tabIndex) {
if (tabIndex < 0 || tabIndex >= tabs.length) return
var newTabs = tabs.slice()
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 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 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: fileExistsComponent
Process {
property string path
property var callback
command: ["test", "-f", path]
Component.onCompleted: running = true
onExited: (exitCode) => {
callback(exitCode === 0)
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: {
console.error("Failed to save tab content")
if (creationCallback) {
creationCallback()
}
destroy()
}
}
}
function createEmptyFile(path, callback) {
var cleanPath = path.toString()
if (!cleanPath.startsWith("/")) {
cleanPath = baseDir + "/" + cleanPath
}
var creator = fileCreatorComponent.createObject(root, {
filePath: cleanPath,
creationCallback: callback
})
}
function copyFile(source, destination) {
copyProcess.source = source
copyProcess.destination = destination
copyProcess.running = true
}
function deleteFile(path) {
deleteProcess.filePath = path
deleteProcess.running = true
}
Component {
id: fileCreatorComponent
QtObject {
property string filePath
property var creationCallback
Component.onCompleted: {
var touchProcess = touchProcessComponent.createObject(this, {
filePath: filePath,
callback: creationCallback
})
}
}
}
Component {
id: touchProcessComponent
Process {
property string filePath
property var callback
command: ["touch", filePath]
Component.onCompleted: running = true
onExited: (exitCode) => {
if (callback) callback()
destroy()
}
}
}
Process {
id: copyProcess
property string source
property string destination
command: ["cp", source, destination]
}
Process {
id: deleteProcess
property string filePath
command: ["rm", "-f", filePath]
}
Process {
id: mkdirProcess
command: ["mkdir", "-p", root.baseDir, root.filesDir]
}
}