Auto-Tag progress window added
More auto-tag and other stuff git-svn-id: http://comictagger.googlecode.com/svn/trunk@322 6c5673fe-1810-88d6-992b-cd32ca31540c
This commit is contained in:
parent
b712226b1e
commit
221923607a
60
autotagprogresswindow.py
Normal file
60
autotagprogresswindow.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""
|
||||
A PyQT4 dialog to show ID log and progress
|
||||
"""
|
||||
|
||||
"""
|
||||
Copyright 2012 Anthony Beville
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from PyQt4 import QtCore, QtGui, uic
|
||||
import os
|
||||
from settings import ComicTaggerSettings
|
||||
|
||||
|
||||
class AutoTagProgressWindow(QtGui.QDialog):
|
||||
|
||||
|
||||
def __init__(self, parent):
|
||||
super(AutoTagProgressWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'autotagprogresswindow.ui' ), self)
|
||||
self.lblTest.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
self.lblArchive.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
self.isdone = False
|
||||
|
||||
def setArchiveImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblArchive )
|
||||
|
||||
def setTestImage( self, img_data):
|
||||
self.setCoverImage( img_data, self.lblTest )
|
||||
|
||||
def setCoverImage( self, img_data , label):
|
||||
if img_data is not None:
|
||||
img = QtGui.QImage()
|
||||
img.loadFromData( img_data )
|
||||
label.setPixmap(QtGui.QPixmap(img))
|
||||
label.setScaledContents(True)
|
||||
else:
|
||||
label.setPixmap(QtGui.QPixmap(os.path.join(ComicTaggerSettings.baseDir(), 'graphics/nocover.png' )))
|
||||
label.setScaledContents(True)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
def reject(self):
|
||||
QtGui.QDialog.reject(self)
|
||||
self.isdone = True
|
||||
|
||||
|
152
autotagprogresswindow.ui
Normal file
152
autotagprogresswindow.ui
Normal file
@ -0,0 +1,152 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>dialogIssueSelect</class>
|
||||
<widget class="QDialog" name="dialogIssueSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>865</width>
|
||||
<height>413</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Issue Identification Progress</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="invertedAppearance">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="textEdit">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Courier New</family>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel</set>
|
||||
</property>
|
||||
<property name="centerButtons">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="lblArchive">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="lblTest">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>165</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>TextLabel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>dialogIssueSelect</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -38,12 +38,15 @@ class AutoTagStartWindow(QtGui.QDialog):
|
||||
self.settings = settings
|
||||
|
||||
self.cbxNoAutoSaveOnLow.setCheckState( QtCore.Qt.Unchecked )
|
||||
self.cbxDontUseYear.setCheckState( QtCore.Qt.Unchecked )
|
||||
|
||||
self.noAutoSaveOnLow = False
|
||||
self.dontUseYear = False
|
||||
|
||||
|
||||
def accept( self ):
|
||||
QtGui.QDialog.accept(self)
|
||||
|
||||
self.noAutoSaveOnLow = self.cbxNoAutoSaveOnLow.isChecked()
|
||||
self.dontUseYear = self.cbxDontUseYear.isChecked()
|
||||
|
@ -50,6 +50,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="cbxDontUseYear">
|
||||
<property name="text">
|
||||
<string>Don't use publication year in indentification process</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
|
@ -294,7 +294,7 @@ def process_file_cli( filename, opts, settings, match_results ):
|
||||
if not opts.dryrun:
|
||||
md = ca.readMetadata( opts.copy_source )
|
||||
|
||||
if settings.apply_cbl_transform_on_bulk_operation:
|
||||
if settings.apply_cbl_transform_on_bulk_operation and opts.data_style == MetaDataStyle.CBI:
|
||||
md = CBLTransformer( md, settings ).apply()
|
||||
|
||||
if not ca.writeMetadata( md, opts.data_style ):
|
||||
|
@ -106,7 +106,10 @@ class FileRenamer:
|
||||
|
||||
new_name += ext
|
||||
|
||||
# some tweaks to keep various filesystems happy
|
||||
new_name = new_name.replace("/", "-")
|
||||
new_name = new_name.replace(":", "-")
|
||||
|
||||
return new_name
|
||||
|
||||
|
||||
|
@ -41,13 +41,19 @@
|
||||
<property name="text">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>File Name</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>CR</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string/>
|
||||
<string>Has ComicRack Tags</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
@ -57,6 +63,9 @@
|
||||
<property name="text">
|
||||
<string>CBL</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Has ComicBookLover Tags</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
@ -65,16 +74,34 @@
|
||||
<property name="text">
|
||||
<string>Type</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Archive Type</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>R/O</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Read-Only</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Folder</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>File Location</string>
|
||||
</property>
|
||||
<property name="textAlignment">
|
||||
<set>AlignHCenter|AlignVCenter|AlignCenter</set>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -72,6 +72,7 @@ class IssueIdentifier:
|
||||
self.additional_metadata = GenericMetadata()
|
||||
self.output_function = IssueIdentifier.defaultWriteOutput
|
||||
self.callback = None
|
||||
self.coverUrlCallback = None
|
||||
self.search_result = self.ResultNoMatches
|
||||
self.cover_page_index = 0
|
||||
|
||||
@ -97,7 +98,7 @@ class IssueIdentifier:
|
||||
def setOutputFunction( self, func ):
|
||||
self.output_function = func
|
||||
pass
|
||||
|
||||
|
||||
def calculateHash( self, image_data ):
|
||||
if self.image_hasher == '3':
|
||||
return ImageHasher( data=image_data ).dct_average_hash()
|
||||
@ -130,6 +131,9 @@ class IssueIdentifier:
|
||||
|
||||
def setProgressCallback( self, cb_func ):
|
||||
self.callback = cb_func
|
||||
|
||||
def setCoverURLCallback( self, cb_func ):
|
||||
self.coverUrlCallback = cb_func
|
||||
|
||||
def getSearchKeys( self ):
|
||||
|
||||
@ -306,7 +310,6 @@ class IssueIdentifier:
|
||||
if self.callback is not None:
|
||||
self.callback( 0, len(series_shortlist))
|
||||
|
||||
|
||||
# now sort the list by name length
|
||||
series_shortlist.sort(key=lambda x: len(x['name']), reverse=False)
|
||||
|
||||
@ -358,6 +361,9 @@ class IssueIdentifier:
|
||||
self.match_list = []
|
||||
return self.match_list
|
||||
|
||||
if self.coverUrlCallback is not None:
|
||||
self.coverUrlCallback( url_image_data )
|
||||
|
||||
url_image_hash = self.calculateHash( url_image_data )
|
||||
score = ImageHasher.hamming_distance(cover_hash, url_image_hash)
|
||||
|
||||
@ -387,7 +393,6 @@ class IssueIdentifier:
|
||||
|
||||
break
|
||||
self.log_msg( "" )
|
||||
|
||||
|
||||
if len(self.match_list) == 0:
|
||||
self.log_msg( ":-( no matches!" )
|
||||
|
@ -23,6 +23,8 @@ from PyQt4 import QtCore, QtGui, uic
|
||||
from settings import ComicTaggerSettings
|
||||
from settingswindow import SettingsWindow
|
||||
from filerenamer import FileRenamer
|
||||
from options import MetaDataStyle
|
||||
|
||||
import os
|
||||
import utils
|
||||
|
||||
@ -32,6 +34,7 @@ class RenameWindow(QtGui.QDialog):
|
||||
super(RenameWindow, self).__init__(parent)
|
||||
|
||||
uic.loadUi(os.path.join(ComicTaggerSettings.baseDir(), 'renamewindow.ui' ), self)
|
||||
self.label.setText("Preview (based on {0} tags):".format(MetaDataStyle.name[data_style]))
|
||||
|
||||
self.settings = settings
|
||||
self.comic_archive_list = comic_archive_list
|
||||
|
@ -334,7 +334,7 @@
|
||||
<item row="1" column="0">
|
||||
<widget class="QCheckBox" name="cbxApplyCBLTransformOnBatchOperation">
|
||||
<property name="text">
|
||||
<string>Apply CBL Transforms on Batch/CLI Operations</string>
|
||||
<string>Apply CBL Transforms on Batch Copy Operations to CBL Tags</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -51,6 +51,7 @@ from exportwindow import ExportWindow, ExportConflictOpts
|
||||
from pageloader import PageLoader
|
||||
from issueidentifier import IssueIdentifier
|
||||
from autotagstartwindow import AutoTagStartWindow
|
||||
from autotagprogresswindow import AutoTagProgressWindow
|
||||
import utils
|
||||
import ctversion
|
||||
|
||||
@ -489,7 +490,6 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
self.lblCover.setPixmap(QtGui.QPixmap(img))
|
||||
self.lblCover.setScaledContents(True)
|
||||
|
||||
|
||||
def updateMenus( self ):
|
||||
|
||||
# First just disable all the questionable items
|
||||
@ -1468,7 +1468,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
print "Network error while getting issue details. Save aborted"
|
||||
|
||||
if cv_md is not None:
|
||||
if self.settings.apply_cbl_transform_on_bulk_operation:
|
||||
if self.settings.apply_cbl_transform_on_cv_import:
|
||||
cv_md = CBLTransformer( cv_md, self.settings ).apply()
|
||||
|
||||
QtGui.QApplication.restoreOverrideCursor()
|
||||
@ -1476,7 +1476,7 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
return cv_md
|
||||
|
||||
|
||||
def identifyAndTagSingleArchive( self, ca, match_results, abortOnLowConfidence ):
|
||||
def identifyAndTagSingleArchive( self, ca, match_results, abortOnLowConfidence, dontUseYear ):
|
||||
success = False
|
||||
ii = IssueIdentifier( ca, self.settings )
|
||||
|
||||
@ -1491,14 +1491,20 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
|
||||
def myoutput( text ):
|
||||
IssueIdentifier.defaultWriteOutput( text )
|
||||
self.atprogdialog.textEdit.ensureCursorVisible()
|
||||
self.atprogdialog.textEdit.insertPlainText(text)
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if dontUseYear:
|
||||
md.year = None
|
||||
ii.setAdditionalMetadata( md )
|
||||
ii.onlyUseAdditionalMetaData = True
|
||||
ii.setOutputFunction( myoutput )
|
||||
ii.cover_page_index = md.getCoverPageIndexList()[0]
|
||||
ii.setCoverURLCallback( self.atprogdialog.setTestImage )
|
||||
|
||||
matches = ii.search()
|
||||
|
||||
result = ii.search_result
|
||||
@ -1569,41 +1575,66 @@ class TaggerWindow( QtGui.QMainWindow):
|
||||
dlg.setModal( True )
|
||||
if not dlg.exec_():
|
||||
return
|
||||
|
||||
progdialog = QtGui.QProgressDialog("", "Cancel", 0, len(ca_list), self)
|
||||
progdialog.setWindowTitle( "Auto-Tagging" )
|
||||
progdialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
progdialog.show()
|
||||
|
||||
|
||||
self.atprogdialog = AutoTagProgressWindow( self)
|
||||
self.atprogdialog.setModal(True)
|
||||
#self.progdialog.rejected.connect( self.identifyCancel )
|
||||
self.atprogdialog.show()
|
||||
self.atprogdialog.progressBar.setMaximum( len(ca_list) )
|
||||
self.atprogdialog.setWindowTitle( "Auto-Tagging" )
|
||||
|
||||
prog_idx = 0
|
||||
|
||||
match_results = OnlineMatchResults()
|
||||
for ca in ca_list:
|
||||
cover_idx = ca.readMetadata(style).getCoverPageIndexList()[0]
|
||||
image_data = ca.getPage( cover_idx )
|
||||
self.atprogdialog.setArchiveImage( image_data )
|
||||
self.atprogdialog.setTestImage( None )
|
||||
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
if progdialog.wasCanceled():
|
||||
if self.atprogdialog.isdone:
|
||||
break
|
||||
progdialog.setValue(prog_idx)
|
||||
self.atprogdialog.progressBar.setValue( prog_idx )
|
||||
prog_idx += 1
|
||||
progdialog.setLabelText( ca.path )
|
||||
progdialog.setAutoClose( False )
|
||||
self.atprogdialog.label.setText( ca.path )
|
||||
QtCore.QCoreApplication.processEvents()
|
||||
|
||||
if ca.isWritable():
|
||||
success, match_results = self.identifyAndTagSingleArchive( ca, match_results, dlg.noAutoSaveOnLow )
|
||||
success, match_results = self.identifyAndTagSingleArchive( ca, match_results, dlg.noAutoSaveOnLow, dlg.dontUseYear )
|
||||
|
||||
#if not success:
|
||||
# QtGui.QMessageBox.warning(self, self.tr("Auto-Tag failed"),
|
||||
# self.tr("The tagging operation seemed to fail for {0} Operation aborted!".format(ca.path)))
|
||||
# break
|
||||
|
||||
print "Good", match_results.goodMatches
|
||||
print "multipleMatches", match_results.multipleMatches
|
||||
print "noMatches", match_results.noMatches
|
||||
print "fetchDataFailures", match_results.fetchDataFailures
|
||||
print "writeFailures", match_results.writeFailures
|
||||
|
||||
progdialog.close()
|
||||
self.atprogdialog.close()
|
||||
self.fileSelectionList.updateSelectedRows()
|
||||
self.loadArchive( self.fileSelectionList.getCurrentArchive() )
|
||||
self.atprogdialog = None
|
||||
|
||||
summary = ""
|
||||
summary += "Successfully tagged archives: {0}\n".format( len(match_results.goodMatches))
|
||||
|
||||
if len ( match_results.multipleMatches ) > 0:
|
||||
summary += "Archives with multiple matches: {0}\n".format( len(match_results.multipleMatches))
|
||||
if len ( match_results.noMatches ) > 0:
|
||||
summary += "Archives with no matches: {0}\n".format( len(match_results.noMatches))
|
||||
if len ( match_results.fetchDataFailures ) > 0:
|
||||
summary += "Archives that failed due to data fetch errors: {0}\n".format( len(match_results.fetchDataFailures))
|
||||
if len ( match_results.writeFailures ) > 0:
|
||||
summary += "Archives that failed due to file writing errors: {0}\n".format( len(match_results.writeFailures))
|
||||
|
||||
if len ( match_results.multipleMatches ) > 0:
|
||||
summary += "\n\nDo you want to manually select the ones with multiple matches now?"
|
||||
|
||||
reply = QtGui.QMessageBox.question(self,
|
||||
self.tr("Auto-Tag Summary"),
|
||||
self.tr(summary),
|
||||
QtGui.QMessageBox.Yes, QtGui.QMessageBox.No )
|
||||
|
||||
if reply == QtGui.QMessageBox.Yes:
|
||||
print "TBD"
|
||||
else:
|
||||
QtGui.QMessageBox.information(self, self.tr("Auto-Tag Summary"), self.tr(summary))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
24
todo.txt
24
todo.txt
@ -2,36 +2,34 @@
|
||||
Features
|
||||
-----------------------------------------------------
|
||||
|
||||
Re-arrange main form layout
|
||||
|
||||
New menu graphics
|
||||
auto tag
|
||||
open folder vs file
|
||||
|
||||
Multi-file:
|
||||
|
||||
File list:
|
||||
|
||||
Delete archive function??
|
||||
change menu order
|
||||
|
||||
|
||||
Batch Functions:
|
||||
|
||||
Batch Auto-Select
|
||||
Start/Options Dialog
|
||||
Progress Dialog - maybe reuse
|
||||
-Show compared images to show progress in a sexy way
|
||||
Auto-Tag
|
||||
Interactive dialog at end
|
||||
Summary Dialog
|
||||
|
||||
Rename
|
||||
check-box for rows?
|
||||
|
||||
Batch Tag Copy
|
||||
Disallow overwrites?
|
||||
|
||||
manual edit the preview?
|
||||
|
||||
-----------------------------------------------------
|
||||
Bugs
|
||||
-----------------------------------------------------
|
||||
Ultimate Spider-Man files can't be read
|
||||
square bracket in folder name
|
||||
|
||||
(python:4401): GLib-ERROR **: Creating pipes for GWakeup: Too many open files
|
||||
|
||||
|
||||
-----------------------------------------------------
|
||||
Big Future Features
|
||||
-----------------------------------------------------
|
||||
|
Loading…
x
Reference in New Issue
Block a user