diff --git a/ftl/core/preferences.ftl b/ftl/core/preferences.ftl
index 76e3132a0..779dc4f70 100644
--- a/ftl/core/preferences.ftl
+++ b/ftl/core/preferences.ftl
@@ -46,3 +46,5 @@ preferences-daily-backups = Daily backups to keep:
preferences-weekly-backups = Weekly backups to keep:
preferences-monthly-backups = Monthly backups to keep:
preferences-minutes-between-backups = Minutes between automatic backups:
+preferences-reduce-motion = Reduce motion
+preferences-reduce-motion-tooltip = Disable various animations and transitions of the user interface
diff --git a/qt/aqt/forms/preferences.ui b/qt/aqt/forms/preferences.ui
index eb4a4bd35..d1b7f4aac 100644
--- a/qt/aqt/forms/preferences.ui
+++ b/qt/aqt/forms/preferences.ui
@@ -7,7 +7,7 @@
0
0
640
- 530
+ 640
@@ -42,6 +42,9 @@
12
+ -
+
+
-
@@ -55,9 +58,6 @@
- -
-
-
-
@@ -103,6 +103,16 @@
+ -
+
+
+ preferences_reduce_motion_tooltip
+
+
+ preferences_reduce_motion
+
+
+
-
-
@@ -676,8 +686,8 @@
- lang
theme
+ lang
video_driver
showPlayButtons
interrupt_audio
@@ -685,6 +695,7 @@
paste_strips_formatting
ignore_accents_in_search
legacy_import_export
+ reduce_motion
useCurrent
default_search_text
uiScale
@@ -703,11 +714,11 @@
fullSync
syncDeauth
media_log
- tabWidget
minutes_between_backups
daily_backups
weekly_backups
monthly_backups
+ tabWidget
diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py
index f3b1fee0f..9f485764b 100644
--- a/qt/aqt/preferences.py
+++ b/qt/aqt/preferences.py
@@ -207,6 +207,7 @@ class Preferences(QDialog):
def setup_global(self) -> None:
"Setup options global to all profiles."
+ self.form.reduce_motion.setChecked(self.mw.pm.reduced_motion())
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
themes = [
tr.preferences_theme_label(theme=theme)
@@ -236,6 +237,8 @@ class Preferences(QDialog):
self.mw.pm.setUiScale(newScale)
restart_required = True
+ self.mw.pm.set_reduced_motion(self.form.reduce_motion.isChecked())
+
self.mw.pm.set_legacy_import_export(self.form.legacy_import_export.isChecked())
if restart_required:
diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py
index f177950d5..75cc87cde 100644
--- a/qt/aqt/profiles.py
+++ b/qt/aqt/profiles.py
@@ -518,6 +518,12 @@ create table if not exists profiles
def setUiScale(self, scale: float) -> None:
self.meta["uiScale"] = scale
+ def reduced_motion(self) -> bool:
+ return self.meta.get("reduced_motion", False)
+
+ def set_reduced_motion(self, on: bool) -> None:
+ self.meta["reduced_motion"] = on
+
def last_addon_update_check(self) -> int:
return self.meta.get("last_addon_update_check", 0)
diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py
index 6a943e042..36b536d59 100644
--- a/qt/aqt/theme.py
+++ b/qt/aqt/theme.py
@@ -122,7 +122,7 @@ class ThemeManager:
return cache.setdefault(path, icon)
def body_class(self, night_mode: bool | None = None) -> str:
- "Returns space-separated class list for platform/theme."
+ "Returns space-separated class list for platform/theme/global settings."
classes = []
if is_win:
classes.append("isWin")
@@ -137,6 +137,8 @@ class ThemeManager:
classes.extend(["nightMode", "night_mode"])
if self.macos_dark_mode():
classes.append("macos-dark-mode")
+ if aqt.mw.pm.reduced_motion():
+ classes.append("reduced-motion")
return " ".join(classes)
def body_classes_for_card_ord(
diff --git a/sass/base.scss b/sass/base.scss
index ab8054a6b..7b907140e 100644
--- a/sass/base.scss
+++ b/sass/base.scss
@@ -74,3 +74,8 @@ samp {
.night-mode .form-select:disabled {
background-color: var(--disabled);
}
+
+.reduced-motion * {
+ transition: none !important;
+ animation: none !important;
+}
diff --git a/ts/components/Collapsible.svelte b/ts/components/Collapsible.svelte
index 1b19f28c9..22396e11d 100644
--- a/ts/components/Collapsible.svelte
+++ b/ts/components/Collapsible.svelte
@@ -46,7 +46,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
isCollapsed = true;
const height = collapse ? inner.clientHeight : getRequiredHeight(inner);
- const duration = Math.sqrt(height * 80);
+
+ /* This function practically caps the maximum time at around 200ms,
+ but still allows to differentiate between small and large contents */
+ const duration = 10 + Math.pow(height, 1 / 4) * 20;
setStyle(height, duration);