Remove timeline view so we can clean up the indexing class
This commit is contained in:
@@ -12,7 +12,6 @@ from nw.gui.dialogs.export import GuiExport
|
||||
from nw.gui.dialogs.itemeditor import GuiItemEditor
|
||||
from nw.gui.dialogs.projecteditor import GuiProjectEditor
|
||||
from nw.gui.dialogs.sessionlog import GuiSessionLogView
|
||||
from nw.gui.dialogs.timelineview import GuiTimeLineView
|
||||
|
||||
# GUI Elements
|
||||
from nw.gui.elements.docdetails import GuiDocDetails
|
||||
@@ -38,7 +37,6 @@ __all__ = [
|
||||
"GuiItemEditor",
|
||||
"GuiProjectEditor",
|
||||
"GuiSessionLogView",
|
||||
"GuiTimeLineView",
|
||||
"GuiDocDetails",
|
||||
"GuiDocEditor",
|
||||
"GuiDocTree",
|
||||
|
||||
@@ -5,7 +5,6 @@ from nw.gui.dialogs.export import GuiExport
|
||||
from nw.gui.dialogs.itemeditor import GuiItemEditor
|
||||
from nw.gui.dialogs.projecteditor import GuiProjectEditor
|
||||
from nw.gui.dialogs.sessionlog import GuiSessionLogView
|
||||
from nw.gui.dialogs.timelineview import GuiTimeLineView
|
||||
|
||||
__all__ = [
|
||||
"GuiConfigEditor",
|
||||
@@ -13,5 +12,4 @@ __all__ = [
|
||||
"GuiItemEditor",
|
||||
"GuiProjectEditor",
|
||||
"GuiSessionLogView",
|
||||
"GuiTimeLineView",
|
||||
]
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Timeline View
|
||||
|
||||
novelWriter – GUI Timeline View
|
||||
=================================
|
||||
Class holding the timeline view window
|
||||
|
||||
File History:
|
||||
Created: 2019-05-30 [0.1.4]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QColor, QPixmap
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QLabel,
|
||||
QDialogButtonBox, QPushButton, QHeaderView, QGridLayout, QGroupBox,
|
||||
QCheckBox
|
||||
)
|
||||
|
||||
from nw.constants import nwFiles, nwItemClass
|
||||
from nw.tools import OptLastState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiTimeLineView(QDialog):
|
||||
|
||||
def __init__(self, theParent, theProject, theIndex):
|
||||
QDialog.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising TimeLineView ...")
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.theIndex = theIndex
|
||||
self.optState = TimeLineLastState(self.theProject,nwFiles.TLINE_OPT)
|
||||
self.optState.loadSettings()
|
||||
|
||||
self.theMatrix = {}
|
||||
self.numRows = 0
|
||||
self.numCols = 0
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.filterBox = QVBoxLayout()
|
||||
self.centreBox = QHBoxLayout()
|
||||
self.bottomBox = QHBoxLayout()
|
||||
|
||||
self.setWindowTitle("Timeline View")
|
||||
self.setMinimumWidth(700)
|
||||
self.setMinimumHeight(400)
|
||||
|
||||
winWidth = self.optState.validIntRange(
|
||||
self.optState.getSetting("winWidth"), 700, 10000, 700
|
||||
)
|
||||
winHeight = self.optState.validIntRange(
|
||||
self.optState.getSetting("winHeight"), 400, 10000, 400
|
||||
)
|
||||
self.resize(winWidth,winHeight)
|
||||
|
||||
# TimeLine Table
|
||||
self.mainTable = QTableWidget()
|
||||
self.mainTable.setGridStyle(Qt.NoPen)
|
||||
|
||||
self.hHeader = self.mainTable.horizontalHeader()
|
||||
self.hHeader.setSectionResizeMode(QHeaderView.ResizeToContents)
|
||||
self.mainTable.setHorizontalHeader(self.hHeader)
|
||||
|
||||
self.vHeader = self.mainTable.verticalHeader()
|
||||
self.vHeader.setSectionResizeMode(QHeaderView.ResizeToContents)
|
||||
self.mainTable.setVerticalHeader(self.vHeader)
|
||||
|
||||
# Option Box
|
||||
self.optFilter = QGroupBox("Include Tags", self)
|
||||
self.optFilterGrid = QGridLayout(self)
|
||||
self.optFilter.setLayout(self.optFilterGrid)
|
||||
|
||||
self.filterPlot = QCheckBox("Plot tags", self)
|
||||
self.filterPlot.setChecked(self.optState.getSetting("fPlot"))
|
||||
self.filterPlot.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.filterChar = QCheckBox("Character tags", self)
|
||||
self.filterChar.setChecked(self.optState.getSetting("fChar"))
|
||||
self.filterChar.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.filterWorld = QCheckBox("Location tags", self)
|
||||
self.filterWorld.setChecked(self.optState.getSetting("fWorld"))
|
||||
self.filterWorld.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.filterTime = QCheckBox("Timeline tags", self)
|
||||
self.filterTime.setChecked(self.optState.getSetting("fTime"))
|
||||
self.filterTime.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.filterObject = QCheckBox("Object tags", self)
|
||||
self.filterObject.setChecked(self.optState.getSetting("fObject"))
|
||||
self.filterObject.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.filterCustom = QCheckBox("Custom tags", self)
|
||||
self.filterCustom.setChecked(self.optState.getSetting("fCustom"))
|
||||
self.filterCustom.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.optFilterGrid.addWidget(self.filterPlot, 0, 1)
|
||||
self.optFilterGrid.addWidget(self.filterChar, 1, 1)
|
||||
self.optFilterGrid.addWidget(self.filterWorld, 2, 1)
|
||||
self.optFilterGrid.addWidget(self.filterTime, 3, 1)
|
||||
self.optFilterGrid.addWidget(self.filterObject, 4, 1)
|
||||
self.optFilterGrid.addWidget(self.filterCustom, 5, 1)
|
||||
|
||||
self.optHide = QGroupBox("Filters", self)
|
||||
self.optHideGrid = QGridLayout(self)
|
||||
self.optHide.setLayout(self.optHideGrid)
|
||||
|
||||
self.hideUnused = QCheckBox("Hide unused", self)
|
||||
self.hideUnused.setChecked(self.optState.getSetting("hUnused"))
|
||||
self.hideUnused.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.optHideGrid.addWidget(self.hideUnused, 0, 1)
|
||||
|
||||
# Button Box
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
|
||||
self.btnRebuild = QPushButton("Rebuild Index")
|
||||
self.btnRebuild.clicked.connect(self.theParent.rebuildIndex)
|
||||
|
||||
self.btnRefresh = QPushButton("Refresh Table")
|
||||
self.btnRefresh.clicked.connect(self._buildNovelList)
|
||||
|
||||
self.bottomBox.addWidget(self.btnRebuild)
|
||||
self.bottomBox.addWidget(self.btnRefresh)
|
||||
self.bottomBox.addStretch()
|
||||
self.bottomBox.addWidget(self.buttonBox)
|
||||
|
||||
# Assemble
|
||||
self.filterBox.addWidget(self.optFilter)
|
||||
self.filterBox.addWidget(self.optHide)
|
||||
self.filterBox.addStretch()
|
||||
self.centreBox.addWidget(self.mainTable)
|
||||
self.centreBox.addLayout(self.filterBox)
|
||||
self.outerBox.addLayout(self.centreBox)
|
||||
self.outerBox.addLayout(self.bottomBox)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self._buildNovelList()
|
||||
self.buttonBox.setFocus()
|
||||
|
||||
self.show()
|
||||
|
||||
logger.debug("TimeLineView initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
def _buildNovelList(self):
|
||||
|
||||
self.mainTable.clear()
|
||||
self.theIndex.buildNovelList()
|
||||
|
||||
self.numRows = len(self.theIndex.novelList)
|
||||
self.mainTable.setRowCount(self.numRows)
|
||||
|
||||
theFilters = {}
|
||||
theFilters["exClass"] = []
|
||||
theFilters["hUnused"] = self.hideUnused.isChecked()
|
||||
|
||||
if not self.filterPlot.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.PLOT)
|
||||
if not self.filterChar.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.CHARACTER)
|
||||
if not self.filterWorld.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.WORLD)
|
||||
if not self.filterTime.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.TIMELINE)
|
||||
if not self.filterObject.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.OBJECT)
|
||||
if not self.filterCustom.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.CUSTOM)
|
||||
|
||||
for n in range(len(self.theIndex.novelList)):
|
||||
iDepth = self.theIndex.novelList[n][1]
|
||||
iTitle = self.theIndex.novelList[n][2]
|
||||
newItem = QTableWidgetItem("%s%s " % (" "*iDepth,iTitle))
|
||||
self.mainTable.setVerticalHeaderItem(n, newItem)
|
||||
|
||||
theMap = self.theIndex.buildTagNovelMap(self.theIndex.tagIndex.keys(), theFilters)
|
||||
self.numCols = len(theMap.keys())
|
||||
self.mainTable.setColumnCount(self.numCols)
|
||||
|
||||
nCol = 0
|
||||
for theTag, theCols in theMap.items():
|
||||
newItem = QTableWidgetItem(" %s " % theTag)
|
||||
self.mainTable.setHorizontalHeaderItem(nCol, newItem)
|
||||
for n in range(len(theCols)):
|
||||
if theCols[n] == 1:
|
||||
pxNew = QPixmap(10,10)
|
||||
pxNew.fill(QColor(0,120,0))
|
||||
lblNew = QLabel()
|
||||
lblNew.setPixmap(pxNew)
|
||||
lblNew.setAlignment(Qt.AlignCenter)
|
||||
lblNew.setAttribute(Qt.WA_TranslucentBackground)
|
||||
self.mainTable.setCellWidget(n, nCol, lblNew)
|
||||
elif theCols[n] == 2:
|
||||
pxNew = QPixmap(10,10)
|
||||
pxNew.fill(QColor(0,0,120))
|
||||
lblNew = QLabel()
|
||||
lblNew.setPixmap(pxNew)
|
||||
lblNew.setAlignment(Qt.AlignCenter)
|
||||
lblNew.setAttribute(Qt.WA_TranslucentBackground)
|
||||
self.mainTable.setCellWidget(n, nCol, lblNew)
|
||||
nCol += 1
|
||||
|
||||
return
|
||||
|
||||
def _doClose(self):
|
||||
|
||||
logger.verbose("GuiTimeLineView close button clicked")
|
||||
|
||||
winWidth = self.width()
|
||||
winHeight = self.height()
|
||||
fPlot = self.filterPlot.isChecked()
|
||||
fChar = self.filterChar.isChecked()
|
||||
fWorld = self.filterWorld.isChecked()
|
||||
fTime = self.filterTime.isChecked()
|
||||
fObject = self.filterObject.isChecked()
|
||||
fCustom = self.filterCustom.isChecked()
|
||||
hUnused = self.hideUnused.isChecked()
|
||||
|
||||
self.optState.setSetting("winWidth", winWidth)
|
||||
self.optState.setSetting("winHeight",winHeight)
|
||||
self.optState.setSetting("fPlot", fPlot)
|
||||
self.optState.setSetting("fChar", fChar)
|
||||
self.optState.setSetting("fWorld", fWorld)
|
||||
self.optState.setSetting("fTime", fTime)
|
||||
self.optState.setSetting("fObject", fObject)
|
||||
self.optState.setSetting("fCustom", fCustom)
|
||||
self.optState.setSetting("hUnused", hUnused)
|
||||
|
||||
self.optState.saveSettings()
|
||||
self.close()
|
||||
|
||||
return
|
||||
|
||||
def _filterChange(self, checkState):
|
||||
self._buildNovelList()
|
||||
return
|
||||
|
||||
# END Class GuiTimeLineView
|
||||
|
||||
class TimeLineLastState(OptLastState):
|
||||
|
||||
def __init__(self, theProject, theFile):
|
||||
OptLastState.__init__(self, theProject, theFile)
|
||||
self.theState = {
|
||||
"winWidth" : 700,
|
||||
"winHeight" : 400,
|
||||
"fPlot" : True,
|
||||
"fChar" : True,
|
||||
"fWorld" : True,
|
||||
"fTime" : True,
|
||||
"fObject" : True,
|
||||
"fCustom" : True,
|
||||
"hUnused" : True,
|
||||
}
|
||||
self.stringOpt = ()
|
||||
self.boolOpt = ("fPlot","fChar","fWorld","fTime","fObject","fCustom","hUnused")
|
||||
self.intOpt = ("winWidth","winHeight")
|
||||
return
|
||||
|
||||
# END Class TimeLineLastState
|
||||
@@ -87,7 +87,7 @@ class GuiDocViewDetails(QWidget):
|
||||
if self.isSticky.isChecked():
|
||||
return
|
||||
|
||||
theRefs = self.theParent.theIndex.buildReferenceList(tHandle)
|
||||
theRefs = self.theParent.theIndex.getBackReferenceList(tHandle)
|
||||
theList = []
|
||||
for tHandle in theRefs:
|
||||
tItem = self.theProject.getItem(tHandle)
|
||||
|
||||
@@ -416,16 +416,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aFullScreen.triggered.connect(self.theParent.toggleFullScreenMode)
|
||||
self.viewMenu.addAction(self.aFullScreen)
|
||||
|
||||
# View > Separator
|
||||
self.viewMenu.addSeparator()
|
||||
|
||||
# View > Project Timeline
|
||||
self.aViewTimeLine = QAction("Show Project Timeline", self)
|
||||
self.aViewTimeLine.setStatusTip("Open the project timeline window")
|
||||
self.aViewTimeLine.setShortcut("Ctrl+T")
|
||||
self.aViewTimeLine.triggered.connect(self.theParent.showTimeLineDialog)
|
||||
self.viewMenu.addAction(self.aViewTimeLine)
|
||||
|
||||
return
|
||||
|
||||
def _buildEditMenu(self):
|
||||
|
||||
+1
-8
@@ -27,7 +27,7 @@ from nw.gui import (
|
||||
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor,
|
||||
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar,
|
||||
GuiDocViewDetails, GuiConfigEditor, GuiProjectEditor, GuiExport,
|
||||
GuiItemEditor, GuiTimeLineView, GuiSessionLogView, GuiProjectOutline
|
||||
GuiItemEditor, GuiSessionLogView, GuiProjectOutline
|
||||
)
|
||||
from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup
|
||||
from nw.tools import countWords
|
||||
@@ -619,12 +619,6 @@ class GuiMain(QMainWindow):
|
||||
dlgExport.exec_()
|
||||
return True
|
||||
|
||||
def showTimeLineDialog(self):
|
||||
if self.hasProject:
|
||||
dlgTLine = GuiTimeLineView(self, self.theProject, self.theIndex)
|
||||
dlgTLine.exec_()
|
||||
return True
|
||||
|
||||
def showSessionLogDialog(self):
|
||||
if self.hasProject:
|
||||
dlgTLine = GuiSessionLogView(self, self.theProject)
|
||||
@@ -778,7 +772,6 @@ class GuiMain(QMainWindow):
|
||||
self.addAction(self.mainMenu.aFileDetails)
|
||||
self.addAction(self.mainMenu.aZenMode)
|
||||
self.addAction(self.mainMenu.aFullScreen)
|
||||
self.addAction(self.mainMenu.aViewTimeLine)
|
||||
self.addAction(self.mainMenu.aEditUndo)
|
||||
self.addAction(self.mainMenu.aEditRedo)
|
||||
self.addAction(self.mainMenu.aEditCut)
|
||||
|
||||
+18
-70
@@ -17,7 +17,7 @@ import nw
|
||||
from os import path
|
||||
|
||||
from nw.constants import (
|
||||
nwFiles, nwKeyWords, nwItemType, nwItemClass, nwAlert
|
||||
nwFiles, nwKeyWords, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||
)
|
||||
from nw.tools import countWords
|
||||
|
||||
@@ -66,9 +66,6 @@ class NWIndex():
|
||||
self.textCounts = {}
|
||||
self.textSynopsis = {}
|
||||
|
||||
# Lists
|
||||
self.novelList = []
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -261,23 +258,28 @@ class NWIndex():
|
||||
"""
|
||||
|
||||
theItem = self.theProject.getItem(tHandle)
|
||||
if theItem is None: return False
|
||||
if theItem.itemType != nwItemType.FILE: return False
|
||||
if theItem.parHandle == self.theProject.trashRoot: return False
|
||||
if theItem is None:
|
||||
return False
|
||||
if theItem.itemType != nwItemType.FILE:
|
||||
return False
|
||||
if theItem.parHandle == self.theProject.trashRoot:
|
||||
return False
|
||||
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
|
||||
return False
|
||||
|
||||
itemClass = theItem.itemClass
|
||||
itemLayout = theItem.itemLayout
|
||||
|
||||
logger.debug("Indexing item with handle %s" % tHandle)
|
||||
|
||||
# Check file type, and reset its old index
|
||||
if itemClass == nwItemClass.NOVEL:
|
||||
self.novelIndex[tHandle] = []
|
||||
self.refIndex[tHandle] = []
|
||||
isNovel = True
|
||||
else:
|
||||
self.refIndex[tHandle] = []
|
||||
if itemLayout == nwItemLayout.NOTE:
|
||||
self.noteIndex[tHandle] = []
|
||||
self.refIndex[tHandle] = []
|
||||
isNovel = False
|
||||
else:
|
||||
self.novelIndex[tHandle] = []
|
||||
isNovel = True
|
||||
|
||||
# Also clear references to file in tag index
|
||||
clearTags = []
|
||||
@@ -296,7 +298,8 @@ class NWIndex():
|
||||
aLine = aLine
|
||||
nLine += 1
|
||||
nChar = len(aLine.strip())
|
||||
if nChar == 0: continue
|
||||
if nChar == 0:
|
||||
continue
|
||||
|
||||
if aLine.startswith(r"#"):
|
||||
isTitle = self.indexTitle(tHandle, isNovel, aLine, nLine, itemLayout)
|
||||
@@ -513,20 +516,7 @@ class NWIndex():
|
||||
|
||||
return theRefs
|
||||
|
||||
def buildNovelList(self):
|
||||
"""Build a list of the content of the novel.
|
||||
"""
|
||||
self.novelList = []
|
||||
self.novelOrder = []
|
||||
for tHandle in self.theProject.treeOrder:
|
||||
if tHandle not in self.novelIndex:
|
||||
continue
|
||||
for tEntry in self.novelIndex[tHandle]:
|
||||
self.novelList.append(tEntry)
|
||||
self.novelOrder.append("%s:%d" % (tHandle,tEntry[0]))
|
||||
return True
|
||||
|
||||
def buildReferenceList(self, tHandle):
|
||||
def getBackReferenceList(self, tHandle):
|
||||
"""Build a list of files referring back to our file, specified
|
||||
by tHandle.
|
||||
"""
|
||||
@@ -560,46 +550,4 @@ class NWIndex():
|
||||
return theRef[1], theRef[0]
|
||||
return None, 0
|
||||
|
||||
def buildTagNovelMap(self, theTags, theFilters=None):
|
||||
"""Build a two-dimensional map of all titles of the novel and
|
||||
which tags they link to from the various meta tags. This map is
|
||||
used to display the timeline view.
|
||||
"""
|
||||
|
||||
tagMap = {}
|
||||
tagClass = {}
|
||||
exClass = []
|
||||
|
||||
if theFilters is not None:
|
||||
if "exClass" in theFilters.keys():
|
||||
exClass = theFilters["exClass"]
|
||||
|
||||
for theTag in theTags:
|
||||
try:
|
||||
tagClass[theTag] = nwItemClass[self.tagIndex[theTag][2]]
|
||||
except:
|
||||
logger.error("Could not map '%s' to nwItemClass" % self.tagIndex[theTag][2])
|
||||
tagClass[theTag] = None
|
||||
if tagClass[theTag] not in exClass:
|
||||
tagMap[theTag] = [0]*len(self.novelOrder)
|
||||
|
||||
for tHandle in self.refIndex:
|
||||
for nLine, tKey, tTag, nTitle in self.refIndex[tHandle]:
|
||||
if tTag in tagMap.keys() and tKey in self.TAG_CLASS:
|
||||
try:
|
||||
nPos = self.novelOrder.index("%s:%d" % (tHandle, nTitle))
|
||||
if self.TAG_CLASS[tKey][0] == tagClass[tTag]:
|
||||
tagMap[tTag][nPos] = self.TAG_CLASS[tKey][1]
|
||||
except:
|
||||
logger.error("Could not find '%s:%d' in novelOrder" % (tHandle, nTitle))
|
||||
|
||||
if theFilters["hUnused"]:
|
||||
tagMapFiltered = {}
|
||||
for theTag in tagMap.keys():
|
||||
if sum(tagMap[theTag]) > 0:
|
||||
tagMapFiltered[theTag] = tagMap[theTag]
|
||||
return tagMapFiltered
|
||||
|
||||
return tagMap
|
||||
|
||||
# END Class NWIndex
|
||||
|
||||
@@ -95,25 +95,3 @@ def testIndexScanThis(nwTempProj):
|
||||
assert isValid
|
||||
assert str(theBits) == "['@tag', 'this', 'and this']"
|
||||
assert str(thePos) == "[0, 6, 12]"
|
||||
|
||||
@pytest.mark.project
|
||||
def testBuildIndex(nwTempProj):
|
||||
projFile = path.join(nwTempProj,"nwProject.nwx")
|
||||
assert theProject.openProject(projFile)
|
||||
|
||||
theIndex = NWIndex(theProject,theMain)
|
||||
tHandle = "31489056e0916"
|
||||
|
||||
theIndex.scanText(tHandle, (
|
||||
"# Novel\n\n"
|
||||
"## Chapter\n\n"
|
||||
"### Scene\n\n"
|
||||
"#### Section\n\n"
|
||||
"@pov: John\n"
|
||||
"@char: Jane\n"
|
||||
"@location: Somewhere\n"
|
||||
))
|
||||
|
||||
assert theIndex.buildNovelList()
|
||||
assert str(theIndex.novelList) == "[[1, 1, 'Novel', 'SCENE'], [3, 2, 'Chapter', 'SCENE'], [5, 3, 'Scene', 'SCENE'], [7, 4, 'Section', 'SCENE']]"
|
||||
assert str(theIndex.novelOrder) == "['31489056e0916:1', '31489056e0916:3', '31489056e0916:5', '31489056e0916:7']"
|
||||
|
||||
Reference in New Issue
Block a user