mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-05 21:15:38 -05:00
432 lines
12 KiB
QML
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]
|
|
}
|
|
} |