Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding recovery menu to OpenShot for recovering previous auto-save files #5639

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/windows/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,9 @@ def actionNew_trigger(self):
app.updates.reset()
self.updateStatusChanged(False, False)

# Load recent projects again
self.load_recent_menu()

# Refresh files views
self.refreshFilesSignal.emit()
log.info("New Project created.")
Expand Down Expand Up @@ -2787,6 +2790,86 @@ def load_recent_menu(self):
self.recent_menu.addAction(self.actionClearRecents)
self.actionClearRecents.triggered.connect(self.clear_recents_clicked)

# Build recovery menu as well
self.load_restore_menu()

def time_ago_string(self, timestamp):
""" Returns a friendly time difference string for the given timestamp. """
from datetime import datetime
delta = datetime.now() - datetime.fromtimestamp(timestamp)
seconds = delta.total_seconds()

if seconds < 60:
return f"{int(seconds)} seconds ago"
elif seconds < 3600:
minutes = seconds // 60
return f"{int(minutes)} minute{'s' if minutes > 1 else ''} ago"
elif seconds < 86400:
hours = seconds // 3600
return f"{int(hours)} hour{'s' if hours > 1 else ''} ago"
else:
days = seconds // 86400
return f"{int(days)} day{'s' if days > 1 else ''} ago"

def load_restore_menu(self):
""" Clear and load the list of restore version menu items """
recovery_dir = info.RECOVERY_PATH
_ = get_app()._tr # Get translation function
app = get_app() # Get the application instance

# Get list of recovery files in the directory that match the current project
current_filepath = app.project.current_filepath if app.project else None
recovery_files = [
f for f in os.listdir(recovery_dir)
if f.endswith(".osp") and current_filepath and f.split("-", 1)[1].startswith(os.path.basename(current_filepath).replace(".osp", ""))
]

# Add Restore Previous Version menu (after Open File)
if not self.restore_menu:
# Create a new restore menu
self.restore_menu = self.menuFile.addMenu(QIcon.fromTheme("edit-undo"), _("Recovery"))
self.menuFile.insertMenu(self.actionRecoveryProjects, self.restore_menu)
else:
# Clear the existing children
self.restore_menu.clear()

# Add recovery files to menu
# Show just a placeholder menu, if we have no recovery files
if not recovery_files:
self.restore_menu.addAction(_("No Previous Versions Available")).setDisabled(True)
return

# Sort files in descending order (latest first)
recovery_files.sort(reverse=True)

for file_name in recovery_files:
# Extract timestamp from file name
try:
timestamp = int(file_name.split("-", 1)[0])
friendly_time = self.time_ago_string(timestamp)
file_path = os.path.join(recovery_dir, file_name)

# Add each recovery file
new_action = self.restore_menu.addAction(friendly_time)
new_action.triggered.connect(functools.partial(self.restore_version_clicked, file_path))
except ValueError:
continue

def restore_version_clicked(self, file_path):
""" Handle restoring a previous version when a menu item is clicked """
app = get_app() # Get the application instance
current_filepath = app.project.current_filepath if app.project else None

if current_filepath:
# Copy the recovery file to the current project folder without overwriting existing project
project_folder = os.path.dirname(current_filepath)
new_file_path = os.path.join(project_folder, os.path.basename(file_path))
log.info(f"Recover project from {file_path} to {new_file_path}")
shutil.copy(file_path, new_file_path)

# Emit signal to open the copied project file
self.OpenProjectSignal.emit(new_file_path)

def remove_recent_project(self, file_path):
"""Remove a project from the Recent menu if OpenShot can't find it"""
s = get_app().get_settings()
Expand Down Expand Up @@ -3407,6 +3490,7 @@ def __init__(self, *args):
# Load user settings for window
s = app.get_settings()
self.recent_menu = None
self.restore_menu = None

# Track metrics
track_metric_session() # start session
Expand Down
16 changes: 16 additions & 0 deletions src/windows/ui/main-window.ui
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
<addaction name="actionNew"/>
<addaction name="actionOpen"/>
<addaction name="actionRecentProjects"/>
<addaction name="actionRecoveryProjects"/>
<addaction name="separator"/>
<addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
Expand Down Expand Up @@ -1257,6 +1258,21 @@
<string>Show All</string>
</property>
</action>
<action name="actionRecoveryProjects">
<property name="checkable">
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="edit-undo" resource="../../../images/openshot.qrc">
<normaloff>:/icons/Humanity/actions/16/edit-undo.svg</normaloff>:/icons/Humanity/actions/16/edit-undo.svg</iconset>
</property>
<property name="text">
<string>Recovery Placeholder</string>
</property>
<property name="visible">
<bool>false</bool>
</property>
</action>
<action name="actionRecentProjects">
<property name="checkable">
<bool>false</bool>
Expand Down
Loading