├── .gitignore
├── LICENSE
├── README.md
├── fenrirScreenshotManager.py
├── libs
├── AsyncQtJob.py
├── DiskUtil.py
├── FlowLayout.py
├── TitleFrame.py
└── ___init__.py
├── readme
└── cover.png
├── requirements.txt
└── ui
├── loading.ui
├── log-out.svg
├── logo.svg
├── main.ui
├── notfound.png
├── refresh-ccw.svg
├── results.ui
├── save.svg
├── selectFolder.ui
├── settings.svg
└── style.css
/.gitignore:
--------------------------------------------------------------------------------
1 | /__pycache__*
2 | libs/__pycache__*
3 | /sega saturn/*
4 | /temp/*
5 | /build/*
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Romulo Fernandes
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FenrirScreenshotManager
2 | This is a tool to search and replace the images used by the Fenrir ODE.
3 |
4 | 
5 |
6 | ## Setup
7 |
8 | From the command line, run the following commands to launch the application:
9 |
10 | ```
11 | git clone https://github.com/razor85/FenrirScreenshotManager.git
12 | cd FenrirScreenshotManager
13 | pip install -r requirements.txt
14 | python fenrirScreenshotManager.py
15 | ```
16 |
--------------------------------------------------------------------------------
/fenrirScreenshotManager.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import (QApplication,
2 | QWidget,
3 | QFileDialog,
4 | QLabel,
5 | QGraphicsDropShadowEffect,
6 | QMessageBox)
7 |
8 | from PyQt5.QtGui import QColor, QIcon, QPixmap, QRegion
9 | from PyQt5.QtCore import pyqtSignal, QTimer, Qt, QRect
10 | from PyQt5 import uic
11 | from PIL import Image
12 | from pathlib import Path
13 | from libs.FlowLayout import FlowLayout
14 | from libs.AsyncQtJob import AsyncQtJob
15 | from libs.TitleFrame import TitleFrame
16 | import libs.DiskUtil as disk_util
17 | import shutil
18 | import sys
19 |
20 | stylesheet = None
21 | def getStyleSheet():
22 | global stylesheet
23 | if not stylesheet:
24 | with open('ui/style.css', 'r') as file_ptr:
25 | stylesheet = file_ptr.read()
26 |
27 | return stylesheet
28 |
29 | class CoverLabel(QLabel):
30 | clicked = pyqtSignal()
31 | def __init__(self, cover_url):
32 | super().__init__()
33 | self.cover_url = cover_url
34 |
35 | def mousePressEvent(self, event):
36 | self.clicked.emit()
37 |
38 | loading_window_ui = 'ui/loading.ui'
39 | loading_form, loading_base = uic.loadUiType(loading_window_ui)
40 |
41 | class LoadingWindow(loading_base, loading_form):
42 | def __init__(self, parent, label='Working...'):
43 | super(select_folder_base, self).__init__(parent)
44 | self.setupUi(self)
45 | self.setStyleSheet(getStyleSheet())
46 | self.setWindowFlag(Qt.FramelessWindowHint)
47 | self.progressLabel.setText(label)
48 |
49 | def keyPressEvent(self, event):
50 | None
51 |
52 | select_folder_window_ui = 'ui/selectFolder.ui'
53 | select_folder_form, select_folder_base = uic.loadUiType(select_folder_window_ui)
54 |
55 | class SelectFolderWindow(select_folder_base, select_folder_form):
56 | def __init__(self):
57 | super(select_folder_base, self).__init__()
58 | self.setupUi(self)
59 | self.setStyleSheet(getStyleSheet())
60 | self.setWindowFlag(Qt.FramelessWindowHint)
61 | self.setWindowFlag(Qt.WindowCloseButtonHint, False)
62 | self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
63 | self.browseButton.clicked.connect(self.browseGamesFolder)
64 |
65 | def browseGamesFolder(self):
66 | new_games = QFileDialog.getExistingDirectory()
67 | if new_games:
68 | self.directoryEdit.setText(new_games)
69 |
70 | def done(self, ret_code):
71 | path = self.directoryEdit.text()
72 | screenshots = Path(path) / Path('screenshots')
73 | if not screenshots.exists() and ret_code != 0:
74 | msg = 'Invalid SD card path, \'{}\' directory not found. Create it and continue?'
75 | answer = QMessageBox.question(None, 'Screenshots not found',
76 | msg.format(str(screenshots)),
77 | QMessageBox.Yes | QMessageBox.No)
78 |
79 | if answer == QMessageBox.Yes:
80 | screenshots.mkdir(parents=True)
81 | super().done(ret_code)
82 | else:
83 | super().done(ret_code)
84 |
85 | def exec(self):
86 | super().exec()
87 | return self.directoryEdit.text()
88 |
89 | results_window_ui = 'ui/results.ui'
90 | results_form, results_base = uic.loadUiType(results_window_ui)
91 | class ResultsWindow(results_base, results_form):
92 | def __init__(self, game_name):
93 | super(results_base, self).__init__()
94 | self.setupUi(self)
95 | self.setStyleSheet(getStyleSheet())
96 | self.setWindowFlag(Qt.FramelessWindowHint)
97 | self.setEvents()
98 | self.flowLayout = FlowLayout()
99 | self.flowWidget = QWidget()
100 | self.flowWidget.setLayout(self.flowLayout)
101 | self.flowWidget.setAccessibleName('resultsWidget')
102 | self.resultsScrollArea.setWidget(self.flowWidget)
103 | self.searchEdit.setText('Sega Saturn {}'.format(game_name))
104 | self.labels = []
105 | self.active_cover = None
106 | self.search()
107 |
108 | def setEvents(self):
109 | self.searchButton.clicked.connect(self.search)
110 |
111 | def clearLayout(self):
112 | for i in reversed(range(self.flowLayout.count())):
113 | item = self.flowLayout.takeAt(i)
114 | if item and item.widget():
115 | item.widget().deleteLater()
116 |
117 | def resetLabel(self, label):
118 | label.graphicsEffect().setColor(QColor(0, 0, 0))
119 | label.setStyleSheet("border: 1px solid black;")
120 |
121 | def unselectLabels(self):
122 | for label in self.labels:
123 | self.resetLabel(label)
124 |
125 | def coverClicked(self, label):
126 | self.unselectLabels()
127 | self.active_cover = label.cover_url
128 | label.graphicsEffect().setColor(QColor(255, 0, 0))
129 | label.setStyleSheet("border: 1px solid red;")
130 |
131 | def setComponentsState(self, state):
132 | self.searchEdit.setEnabled(state)
133 | self.maxResultsEdit.setEnabled(state)
134 | self.searchButton.setEnabled(state)
135 | self.resultsScrollArea.setEnabled(state)
136 | self.buttonBox.setEnabled(state)
137 |
138 | def search(self):
139 | self.active_cover = None
140 | self.labels = []
141 | self.clearLayout()
142 | self.setComponentsState(False)
143 | def query_func():
144 | return disk_util._download_thumbnails(self.searchEdit.text(),
145 | int(self.maxResultsEdit.text()))
146 |
147 | self.searchQuery = AsyncQtJob(query_func, self.queryDone)
148 | self.loading = LoadingWindow(self, 'Searching...')
149 | self.loading.exec()
150 |
151 | def queryDone(self, query):
152 | if self.loading:
153 | self.loading.close()
154 | self.loading = None
155 |
156 | if query is not None and query.done():
157 | self.searchQuery = None
158 | self.displaySearchResults()
159 | else:
160 | self.setComponentsState(True)
161 |
162 | def displaySearchResults(self):
163 | self.setComponentsState(True)
164 | items = disk_util.getImagesTempFolder().glob('*.jpg')
165 | for filename in items:
166 | new_label = CoverLabel(filename)
167 | new_label.setPixmap(QPixmap(str(filename)).scaled(256, 192))
168 | new_label.setStyleSheet("border: 3px solid black;")
169 | new_label.clicked.connect(lambda l=new_label: self.coverClicked(l))
170 | shadow_effect = QGraphicsDropShadowEffect()
171 | shadow_effect.setOffset(3, 3)
172 | shadow_effect.setBlurRadius(5)
173 | shadow_effect.setColor(QColor(0, 0, 0))
174 | new_label.setGraphicsEffect(shadow_effect)
175 | self.resetLabel(new_label)
176 | self.flowLayout.addWidget(new_label)
177 | self.labels.append(new_label)
178 |
179 | def accept(self):
180 | if self.searchQuery:
181 | return
182 | else:
183 | return super().accept()
184 |
185 | def reject(self):
186 | if self.searchQuery:
187 | return
188 | else:
189 | return super().reject()
190 |
191 | def done(self, res):
192 | if self.searchQuery:
193 | return
194 | else:
195 | return super().done(res)
196 |
197 | def exec(self):
198 | if super().exec():
199 | return self.active_cover
200 | else:
201 | return None
202 |
203 | main_window_ui = 'ui/main.ui'
204 | main_form, main_base = uic.loadUiType(main_window_ui)
205 |
206 | class MainWindow(main_base, main_form):
207 | def __init__(self):
208 | super(main_base, self).__init__()
209 | self.setupUi(self)
210 | self.setStyleSheet(getStyleSheet())
211 | self.setEvents()
212 | self.verticalLayout.setContentsMargins(10, 10, 10, 10)
213 | self.setWindowFlag(Qt.FramelessWindowHint)
214 |
215 | shadow_effect = QGraphicsDropShadowEffect()
216 | shadow_effect.setOffset(2, 2)
217 | shadow_effect.setBlurRadius(5)
218 | shadow_effect.setColor(QColor(26, 88, 175))
219 | self.titleFrame.setWindow(self)
220 | self.titleLabel.setGraphicsEffect(shadow_effect)
221 | self.games_directory = SelectFolderWindow().exec()
222 | if self.games_directory:
223 | self.games_screenshot_directory = self.games_directory / Path('screenshots')
224 | disk_util.games_folder = Path(self.games_directory)
225 | self.copyOriginalScreenshots()
226 |
227 | self.configButton.setVisible(False)
228 | self.loading_window = None
229 | self.write_images_job = None
230 | self.showGamesList()
231 |
232 | def setComponentsState(self, state):
233 | self.thumbnailDownloadButton.setEnabled(state)
234 | self.thumbnailRemoveButton.setEnabled(state)
235 | self.thumbnailRestoreButton.setEnabled(state)
236 | self.gameList.setEnabled(state)
237 | self.configButton.setEnabled(state)
238 | self.writeImagesButton.setEnabled(state)
239 | self.quitButton.setEnabled(state)
240 |
241 | def copyOriginalScreenshots(self):
242 | temp_directory = disk_util.getScreenshotsTempFolder()
243 | if temp_directory.exists():
244 | shutil.rmtree(temp_directory)
245 |
246 | temp_directory.mkdir(parents=True)
247 | original_files = self.games_screenshot_directory.glob('*.jpg')
248 | for filename in original_files:
249 | shutil.copyfile(filename, temp_directory / filename.name)
250 |
251 | def showGamesList(self):
252 | self.game_list = sorted(disk_util.get_game_list())
253 | self.gameList.clear()
254 | self.gameList.addItems(self.game_list)
255 |
256 | def setEvents(self):
257 | self.gameList.itemSelectionChanged.connect(self.gameSelectionChanged)
258 | self.thumbnailDownloadButton.clicked.connect(self.replaceGameCover)
259 | self.thumbnailRestoreButton.clicked.connect(self.restoreGameCover)
260 | self.thumbnailRemoveButton.clicked.connect(self.removeGameCover)
261 | self.writeImagesButton.clicked.connect(self.writeImages)
262 | self.quitButton.clicked.connect(self.close)
263 |
264 | def backupScreenshots(self):
265 | backup_folder = self.games_directory / Path('screenshots_backup')
266 | count = 0
267 | while backup_folder.exists():
268 | backup_folder = self.games_directory / Path('screenshots_backup{}'.format(count))
269 |
270 | backup_folder.mkdir(parents=True)
271 | original_files = self.games_screenshot_directory.glob('*.jpg')
272 | for filename in original_files:
273 | shutil.copyfile(filename, backup_folder / filename.name)
274 |
275 | def writeImagesImpl(self):
276 | source = disk_util.getScreenshotsTempFolder().glob('*.jpg')
277 | destination = self.games_screenshot_directory
278 | for filename in source:
279 | shutil.copyfile(filename, destination / filename.name)
280 |
281 | def writeImagesDone(self, job):
282 | self.loading_window.close()
283 | self.loading_window = None
284 | self.write_images_job = None
285 | if job.done():
286 | QMessageBox.information(self, 'Complete', 'Files copied successfully')
287 | self.setComponentsState(True)
288 |
289 | def writeImages(self):
290 | msg = ('This action will write the images to your SD card.' +
291 | 'Do you want a backup of your \'screenshots\' folder?' +
292 | 'If you do, sd:/screenshots_backup will be created')
293 |
294 | answer = QMessageBox.question(None, 'Backup first?', msg,
295 | QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
296 |
297 | if answer == QMessageBox.Cancel:
298 | return
299 | elif answer == QMessageBox.Yes:
300 | def writeAll():
301 | self.backupScreenshots()
302 | self.writeImagesImpl()
303 |
304 | self.write_images_job = AsyncQtJob(writeAll, self.writeImagesDone)
305 | elif answer == QMessageBox.No:
306 | self.write_images_job = AsyncQtJob(self.writeImagesImpl, self.writeImagesDone)
307 |
308 | self.loading_window = LoadingWindow(self, 'Writing to SD...')
309 | self.loading_window.show()
310 | self.setComponentsState(False)
311 |
312 | def setThumbnailToFile(self, filename):
313 | if filename and filename.exists():
314 | self.thumbnailLabel.setPixmap(QPixmap(str(filename)).scaled(256, 192))
315 | else:
316 | self.thumbnailLabel.setPixmap(QPixmap('ui/notfound.png').scaled(256, 192))
317 |
318 | def restoreGameCover(self):
319 | selection = self.gameList.selectedItems()
320 | if not selection:
321 | return
322 |
323 | game_name = selection[0].text()
324 | source = self.games_screenshot_directory / Path('{}.jpg'.format(game_name))
325 | destination = disk_util.getScreenshotsTempFolder() / Path('{}.jpg'.format(game_name))
326 | if source.exists():
327 | shutil.copy(source, destination)
328 | self.gameSelectionChanged()
329 |
330 | def removeGameCover(self):
331 | selection = self.gameList.selectedItems()
332 | if not selection:
333 | return
334 |
335 | game_name = selection[0].text()
336 | filename = disk_util.getScreenshotsTempFolder() / Path('{}.jpg'.format(game_name))
337 | if filename.exists():
338 | filename.unlink()
339 | self.gameSelectionChanged()
340 |
341 | def replaceGameCover(self):
342 | selection = self.gameList.selectedItems()
343 | if not selection:
344 | return
345 |
346 | game_name = selection[0].text()
347 | results = ResultsWindow(disk_util.cleanName(game_name))
348 | selected_cover = results.exec()
349 | if selected_cover:
350 | img = Image.open(selected_cover)
351 | img = img.resize((128, 96))
352 | output_name = disk_util.getScreenshotsTempFolder() / Path('{}.jpg'.format(game_name))
353 | img.save(output_name, 'JPEG', quality=100)
354 | self.setThumbnailToFile(output_name)
355 |
356 | def gameSelectionChanged(self):
357 | selection = self.gameList.selectedItems()
358 | self.thumbnailDownloadButton.setEnabled(selection != None)
359 | self.thumbnailRestoreButton.setEnabled(selection != None)
360 | self.thumbnailRemoveButton.setEnabled(selection != None)
361 | if not selection:
362 | return
363 |
364 | game_name = selection[0].text()
365 | thumbnail_file = disk_util.get_thumbnail(game_name)
366 | self.setThumbnailToFile(thumbnail_file)
367 |
368 | def done(self, res):
369 | if self.loading_window or self.write_images_job:
370 | return
371 | else:
372 | return super().done(res)
373 |
374 | if __name__ == '__main__':
375 | app = QApplication(sys.argv)
376 | ex = MainWindow()
377 | ex.show()
378 | if ex.games_directory:
379 | sys.exit(app.exec_())
380 |
--------------------------------------------------------------------------------
/libs/AsyncQtJob.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import QMessageBox
2 | from PyQt5.QtCore import QTimer
3 | from libs.DiskUtil import AsyncJob
4 |
5 | class AsyncQtJob(AsyncJob):
6 | def __init__(self, func, gui_done, timeout=500):
7 | super().__init__(func)
8 | self.gui_done = gui_done
9 | self.timer = QTimer()
10 | self.timer.timeout.connect(self.timerEvent)
11 | self.timer.start(timeout)
12 |
13 | def timerEvent(self):
14 | try:
15 | if self.done():
16 | if self.timer:
17 | self.timer.stop()
18 | self.timer = None
19 |
20 | self.gui_done(self)
21 |
22 | except Exception as e:
23 | QMessageBox.information(None, 'Error', 'Error executing task: {}'.format(str(e)))
24 | if self.timer:
25 | self.timer.stop()
26 | self.timer = None
27 |
28 | self.gui_done(None)
29 |
30 |
31 |
--------------------------------------------------------------------------------
/libs/DiskUtil.py:
--------------------------------------------------------------------------------
1 | from PIL import Image
2 | from duckduckgo_search import DDGS
3 | from pathlib import Path
4 | import io
5 | import os
6 | import re
7 | import requests
8 | import shutil
9 | import threading
10 | import time
11 | import uuid
12 |
13 | print('Current directory: {}'.format(Path.cwd()))
14 | def getCurrentDir():
15 | return Path.cwd()
16 |
17 | screenshot_folder = getCurrentDir() / Path('sega saturn') / Path('screenshots')
18 | def getScreenshotFolder():
19 | global screenshot_folder
20 | return screenshot_folder
21 |
22 | games_folder = getCurrentDir() / Path('sega saturn')
23 | def getGamesFolder():
24 | global games_folder
25 | return games_folder
26 |
27 | temp_folder = getCurrentDir() / Path('temp')
28 | def getTempFolder():
29 | global temp_folder
30 | return temp_folder
31 |
32 | def getImagesTempFolder():
33 | return getTempFolder() / Path('images')
34 |
35 | def getScreenshotsTempFolder():
36 | return getTempFolder() / Path('screenshots')
37 |
38 | def cleanName(name):
39 | return re.sub('\\s*\\(.*\\)\\s*', '', name)
40 |
41 | def get_game_list():
42 | sd_directory = getGamesFolder()
43 | game_list = []
44 | extensions = ['ccd', 'img', 'cue', 'iso']
45 | for extension in extensions:
46 | for filename in sd_directory.glob('**/*.{}'.format(extension)):
47 | directory = filename.parent
48 | dirname = directory.name
49 | if 'System Volume Information' in dirname:
50 | continue
51 |
52 | if directory.is_dir() and dirname not in game_list:
53 | game_list.append(dirname)
54 |
55 | return game_list
56 |
57 | def get_thumbnail(game_name):
58 | output_dir = getScreenshotsTempFolder()
59 | image_file = output_dir / Path('{}.jpg'.format(game_name))
60 | if image_file.exists():
61 | return image_file
62 |
63 | def _download_thumbnails(search_query, max_thumbnails=12):
64 | temp_directory = getImagesTempFolder()
65 | if temp_directory.exists():
66 | shutil.rmtree(temp_directory)
67 |
68 | results = DDGS().images(search_query, max_results=max_thumbnails)
69 |
70 | os.makedirs(temp_directory)
71 | for img in results:
72 | thumbnail_url = img['thumbnail']
73 | filename = str(uuid.uuid4().hex)
74 | while os.path.exists("{}/{}.jpg".format(temp_directory, filename)):
75 | filename = str(uuid.uuid4().hex)
76 |
77 | response = requests.get(thumbnail_url, stream=True, timeout=2.0, allow_redirects=True)
78 | with Image.open(io.BytesIO(response.content)) as im:
79 | with open("{}/{}.jpg".format(temp_directory, filename), 'wb') as out_file:
80 | im = im.resize((256,192))
81 | im.save(out_file)
82 |
83 | return True
84 |
85 | class AsyncJob:
86 | def __init__(self, func):
87 | self.thread = threading.Thread(target=self.wrapFunc, args=(func,))
88 | self.exception = None
89 | self.return_code = 0
90 | self.thread.start()
91 |
92 | def wrapFunc(self, func):
93 | try:
94 | self.return_code = func()
95 | if not self.return_code:
96 | self.return_code = True
97 | except Exception as e:
98 | self.exception = e
99 |
100 | def done(self):
101 | if self.thread.is_alive():
102 | self.thread.join(0.1)
103 |
104 | if self.thread.is_alive():
105 | return False
106 | else:
107 | if self.exception:
108 | raise self.exception
109 | else:
110 | return self.return_code
111 |
112 |
--------------------------------------------------------------------------------
/libs/FlowLayout.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 |
4 | #############################################################################
5 | ##
6 | ## Copyright (C) 2013 Riverbank Computing Limited.
7 | ## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
8 | ## All rights reserved.
9 | ##
10 | ## This file is part of the examples of PyQt.
11 | ##
12 | ## $QT_BEGIN_LICENSE:BSD$
13 | ## You may use this file under the terms of the BSD license as follows:
14 | ##
15 | ## "Redistribution and use in source and binary forms, with or without
16 | ## modification, are permitted provided that the following conditions are
17 | ## met:
18 | ## * Redistributions of source code must retain the above copyright
19 | ## notice, this list of conditions and the following disclaimer.
20 | ## * Redistributions in binary form must reproduce the above copyright
21 | ## notice, this list of conditions and the following disclaimer in
22 | ## the documentation and/or other materials provided with the
23 | ## distribution.
24 | ## * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
25 | ## the names of its contributors may be used to endorse or promote
26 | ## products derived from this software without specific prior written
27 | ## permission.
28 | ##
29 | ## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
30 | ## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
31 | ## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
32 | ## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
33 | ## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
34 | ## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
35 | ## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
36 | ## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
37 | ## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
38 | ## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
39 | ## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
40 | ## $QT_END_LICENSE$
41 | ##
42 | #############################################################################
43 |
44 |
45 | from PyQt5.QtCore import QPoint, QRect, QSize, Qt
46 | from PyQt5.QtWidgets import (QApplication, QLayout, QPushButton, QSizePolicy,
47 | QWidget)
48 |
49 |
50 | class Window(QWidget):
51 | def __init__(self):
52 | super(Window, self).__init__()
53 |
54 | flowLayout = FlowLayout()
55 | flowLayout.addWidget(QPushButton("Short"))
56 | flowLayout.addWidget(QPushButton("Longer"))
57 | flowLayout.addWidget(QPushButton("Different text"))
58 | flowLayout.addWidget(QPushButton("More text"))
59 | flowLayout.addWidget(QPushButton("Even longer button text"))
60 | self.setLayout(flowLayout)
61 |
62 | self.setWindowTitle("Flow Layout")
63 |
64 |
65 | class FlowLayout(QLayout):
66 | def __init__(self, parent=None, margin=0, spacing=-1):
67 | super(FlowLayout, self).__init__(parent)
68 |
69 | if parent is not None:
70 | self.setContentsMargins(margin, margin, margin, margin)
71 |
72 | self.setSpacing(spacing)
73 |
74 | self.itemList = []
75 |
76 | def __del__(self):
77 | item = self.takeAt(0)
78 | while item:
79 | item = self.takeAt(0)
80 |
81 | def addItem(self, item):
82 | self.itemList.append(item)
83 |
84 | def count(self):
85 | return len(self.itemList)
86 |
87 | def itemAt(self, index):
88 | if index >= 0 and index < len(self.itemList):
89 | return self.itemList[index]
90 |
91 | return None
92 |
93 | def takeAt(self, index):
94 | if index >= 0 and index < len(self.itemList):
95 | return self.itemList.pop(index)
96 |
97 | return None
98 |
99 | def expandingDirections(self):
100 | return Qt.Orientations(Qt.Orientation(0))
101 |
102 | def hasHeightForWidth(self):
103 | return True
104 |
105 | def heightForWidth(self, width):
106 | height = self.doLayout(QRect(0, 0, width, 0), True)
107 | return height
108 |
109 | def setGeometry(self, rect):
110 | super(FlowLayout, self).setGeometry(rect)
111 | self.doLayout(rect, False)
112 |
113 | def sizeHint(self):
114 | return self.minimumSize()
115 |
116 | def minimumSize(self):
117 | size = QSize()
118 |
119 | for item in self.itemList:
120 | size = size.expandedTo(item.minimumSize())
121 |
122 | margin, _, _, _ = self.getContentsMargins()
123 |
124 | size += QSize(2 * margin, 2 * margin)
125 | return size
126 |
127 | def doLayout(self, rect, testOnly):
128 | x = rect.x()
129 | y = rect.y()
130 | lineHeight = 0
131 |
132 | for item in self.itemList:
133 | wid = item.widget()
134 | spaceX = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
135 | spaceY = self.spacing() + wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
136 | nextX = x + item.sizeHint().width() + spaceX
137 | if nextX - spaceX > rect.right() and lineHeight > 0:
138 | x = rect.x()
139 | y = y + lineHeight + spaceY
140 | nextX = x + item.sizeHint().width() + spaceX
141 | lineHeight = 0
142 |
143 | if not testOnly:
144 | item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
145 |
146 | x = nextX
147 | lineHeight = max(lineHeight, item.sizeHint().height())
148 |
149 | return y + lineHeight - rect.y()
150 |
151 |
152 | if __name__ == '__main__':
153 |
154 | import sys
155 |
156 | app = QApplication(sys.argv)
157 | mainWin = Window()
158 | mainWin.show()
159 | sys.exit(app.exec_())
160 |
--------------------------------------------------------------------------------
/libs/TitleFrame.py:
--------------------------------------------------------------------------------
1 | from PyQt5.QtWidgets import QFrame
2 | from PyQt5.QtCore import Qt
3 |
4 | class TitleFrame(QFrame):
5 | def __init__(self, p):
6 | super().__init__(p)
7 | self.window = None
8 |
9 | def setWindow(self, window):
10 | self.window = window
11 |
12 | def mousePressEvent(self, event):
13 | if event.button() == Qt.LeftButton:
14 | self.window.moving = True
15 | self.window.offset = event.pos()
16 |
17 | def mouseMoveEvent(self, event):
18 | if self.window.moving:
19 | self.window.move(event.globalPos() - self.window.offset)
20 |
--------------------------------------------------------------------------------
/libs/___init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/razor85/FenrirScreenshotManager/13a4f5a8fb9595219437f28d567a57e634dbdbb3/libs/___init__.py
--------------------------------------------------------------------------------
/readme/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/razor85/FenrirScreenshotManager/13a4f5a8fb9595219437f28d567a57e634dbdbb3/readme/cover.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Pillow==10.0.0
2 | PyQt5==5.15.7
3 | requests==2.32.3
4 | duckduckgo_search==8.0.1
5 | cx-freeze==8.3.0
6 |
--------------------------------------------------------------------------------
/ui/loading.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dialog
4 |
5 |
6 | Qt::ApplicationModal
7 |
8 |
9 |
10 | 0
11 | 0
12 | 400
13 | 83
14 |
15 |
16 |
17 | Copying file
18 |
19 |
20 | true
21 |
22 |
23 | -
24 |
25 |
-
26 |
27 |
28 | Working...
29 |
30 |
31 |
32 | -
33 |
34 |
35 | 0
36 |
37 |
38 | -1
39 |
40 |
41 | false
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/ui/log-out.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
73 |
--------------------------------------------------------------------------------
/ui/main.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | MainWindow
4 |
5 |
6 |
7 | 0
8 | 0
9 | 909
10 | 768
11 |
12 |
13 |
14 | Fenrir Screenshot Manager
15 |
16 |
17 |
18 | logo.svglogo.svg
19 |
20 |
21 |
22 | -
23 |
24 |
25 |
26 |
27 |
28 | QFrame::StyledPanel
29 |
30 |
31 | QFrame::Raised
32 |
33 |
34 |
-
35 |
36 |
37 |
38 | 80
39 | 80
40 |
41 |
42 |
43 |
44 | 80
45 | 80
46 |
47 |
48 |
49 |
50 |
51 |
52 | logo.svg
53 |
54 |
55 | true
56 |
57 |
58 |
59 | -
60 |
61 |
62 |
63 | Consolas
64 | 14
65 |
66 |
67 |
68 | false
69 |
70 |
71 | <html><head/><body><p><span style=" font-weight:600;">Fenrir Screenshot Manager</span></p></body></html>
72 |
73 |
74 | false
75 |
76 |
77 |
78 | -
79 |
80 |
81 | Qt::Horizontal
82 |
83 |
84 |
85 | 40
86 | 20
87 |
88 |
89 |
90 |
91 | -
92 |
93 |
94 |
95 | 48
96 | 48
97 |
98 |
99 |
100 | Settings
101 |
102 |
103 |
104 |
105 |
106 |
107 | settings.svgsettings.svg
108 |
109 |
110 |
111 | -
112 |
113 |
114 |
115 | 48
116 | 48
117 |
118 |
119 |
120 | Write changes to disk
121 |
122 |
123 |
124 |
125 |
126 |
127 | save.svgsave.svg
128 |
129 |
130 |
131 | -
132 |
133 |
134 |
135 | 48
136 | 48
137 |
138 |
139 |
140 | Quit
141 |
142 |
143 |
144 |
145 |
146 |
147 | log-out.svglog-out.svg
148 |
149 |
150 |
151 |
152 |
153 |
154 | -
155 |
156 |
157 | 0
158 |
159 |
-
160 |
161 |
162 | -
163 |
164 |
165 | 10
166 |
167 |
-
168 |
169 |
170 | Qt::Vertical
171 |
172 |
173 |
174 | 20
175 | 40
176 |
177 |
178 |
179 |
180 | -
181 |
182 |
183 |
184 | 256
185 | 192
186 |
187 |
188 |
189 | border: 1px solid #585e64;
190 |
191 |
192 |
193 |
194 |
195 |
196 | -
197 |
198 |
199 | false
200 |
201 |
202 | Replace
203 |
204 |
205 |
206 | -
207 |
208 |
209 | false
210 |
211 |
212 | Restore from SD
213 |
214 |
215 |
216 | -
217 |
218 |
219 | false
220 |
221 |
222 | Remove
223 |
224 |
225 |
226 | -
227 |
228 |
229 | Qt::Vertical
230 |
231 |
232 |
233 | 20
234 | 40
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
255 |
256 |
257 |
258 | TitleFrame
259 | QFrame
260 |
261 | 1
262 |
263 |
264 |
265 |
266 |
267 |
--------------------------------------------------------------------------------
/ui/notfound.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/razor85/FenrirScreenshotManager/13a4f5a8fb9595219437f28d567a57e634dbdbb3/ui/notfound.png
--------------------------------------------------------------------------------
/ui/refresh-ccw.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui/results.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dialog
4 |
5 |
6 |
7 | 0
8 | 0
9 | 1187
10 | 753
11 |
12 |
13 |
14 | Results
15 |
16 |
17 | -
18 |
19 |
-
20 |
21 |
22 | Search:
23 |
24 |
25 |
26 | -
27 |
28 |
29 | -
30 |
31 |
32 | Max Results:
33 |
34 |
35 |
36 | -
37 |
38 |
39 |
40 | 60
41 | 16777215
42 |
43 |
44 |
45 | 10
46 |
47 |
48 | 4
49 |
50 |
51 |
52 | -
53 |
54 |
55 | Search
56 |
57 |
58 |
59 |
60 |
61 | -
62 |
63 |
64 |
65 |
66 |
67 | Qt::ScrollBarAlwaysOff
68 |
69 |
70 | true
71 |
72 |
73 |
74 |
75 | 0
76 | 0
77 | 1159
78 | 637
79 |
80 |
81 |
82 |
83 |
84 | -
85 |
86 |
87 | Qt::Horizontal
88 |
89 |
90 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | buttonBox
100 | accepted()
101 | Dialog
102 | accept()
103 |
104 |
105 | 248
106 | 254
107 |
108 |
109 | 157
110 | 274
111 |
112 |
113 |
114 |
115 | buttonBox
116 | rejected()
117 | Dialog
118 | reject()
119 |
120 |
121 | 316
122 | 260
123 |
124 |
125 | 286
126 | 274
127 |
128 |
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/ui/save.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui/selectFolder.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | selectDialog
4 |
5 |
6 | Qt::ApplicationModal
7 |
8 |
9 |
10 | 0
11 | 0
12 | 370
13 | 140
14 |
15 |
16 |
17 | Select SD card directory
18 |
19 |
20 | true
21 |
22 |
23 | -
24 |
25 |
-
26 |
27 |
28 | Select your SD card directory / drive:
29 |
30 |
31 |
32 | -
33 |
34 |
-
35 |
36 |
37 | -
38 |
39 |
40 | Browse
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | -
49 |
50 |
51 | Qt::Horizontal
52 |
53 |
54 | QDialogButtonBox::Close|QDialogButtonBox::Ok
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | buttonBox
64 | accepted()
65 | selectDialog
66 | accept()
67 |
68 |
69 | 248
70 | 254
71 |
72 |
73 | 157
74 | 274
75 |
76 |
77 |
78 |
79 | buttonBox
80 | rejected()
81 | selectDialog
82 | reject()
83 |
84 |
85 | 316
86 | 260
87 |
88 |
89 | 286
90 | 274
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/ui/settings.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui/style.css:
--------------------------------------------------------------------------------
1 | QPushButton {
2 | background-color: #444444;
3 | border: 1px solid #444444;
4 | border-radius: 2px;
5 | padding: 8px;
6 | padding-left: 12px;
7 | padding-right: 12px;
8 | color: white;
9 | }
10 |
11 | QPushButton:hover {
12 | background-color: #666666;
13 | }
14 |
15 | QPushButton:disabled {
16 | background-color: #444444;
17 | border: 1px solid #444444;
18 | color: #757575;
19 | }
20 |
21 | QLineEdit {
22 | background-color: #444444;
23 | border: 1px solid #444444;
24 | border-radius: 2px;
25 | padding: 6px;
26 | color: white;
27 | }
28 |
29 | QLineEdit:disabled {
30 | color: #757575;
31 | }
32 |
33 | QWidget[accessibleName="resultsWidget"] {
34 | background-color: #444444;
35 | border: 1px solid #444444;
36 | border-radius: 0px;
37 | padding: 4px;
38 | color: white;
39 | }
40 |
41 | QScrollArea {
42 | border-radius: 0px;
43 | }
44 |
45 | QMainWindow, QDialog {
46 | background-color: #222222;
47 | border: 2px solid #999999;
48 | border-radius: 0px;
49 | color: white;
50 | }
51 |
52 | QLabel {
53 | color: white;
54 | }
55 |
56 | QListWidget {
57 | background-color: #333333;
58 | border:none;
59 | padding: 2px;
60 | color: white;
61 | }
62 |
63 | QScrollBar:vertical {
64 | background-color: #333333;
65 | border: none;
66 | padding: 2px;
67 | color: white;
68 | margin: 22px 0 22px 0;
69 | }
70 |
71 | QScrollBar::handle:vertical {
72 | background-color: #444444;
73 | min-height: 10px;
74 | border: none;
75 | }
76 |
77 | QScrollBar::add-line:vertical {
78 | background-color: #222222;
79 | border: 1px solid #555555;
80 | height: 20px;
81 | subcontrol-position: bottom;
82 | subcontrol-origin: margin;
83 | }
84 |
85 | QScrollBar::sub-line:vertical {
86 | background-color: #222222;
87 | border: 1px solid #555555;
88 | height: 20px;
89 | subcontrol-position: top;
90 | subcontrol-origin: margin;
91 | }
92 |
93 | QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {
94 | background-color: white;
95 | border: 1px solid #555555;
96 | width: 3px;
97 | height: 3px;
98 | background: white;
99 | }
100 |
101 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
102 | background: none;
103 | }
104 |
--------------------------------------------------------------------------------