From 9d796108a566289d7762c134de906b06dd7186e3 Mon Sep 17 00:00:00 2001
From: Jorrin <jorrinkievit@hotmail.com>
Date: Mon, 15 Apr 2024 16:49:39 +0200
Subject: [PATCH] setup sources reordering

---
 package.json                                 |   3 +
 pnpm-lock.yaml                               | 103 ++++++++++++++-----
 src/assets/locales/en.json                   |   2 +
 src/components/form/SortableList.tsx         |  89 ++++++++++++++++
 src/hooks/useProviderScrape.tsx              |   5 +
 src/hooks/useSettingsState.ts                |  16 ++-
 src/pages/Settings.tsx                       |  15 +++
 src/pages/parts/settings/PreferencesPart.tsx |  17 +++
 src/stores/preferences/index.tsx             |  13 ++-
 9 files changed, 235 insertions(+), 28 deletions(-)
 create mode 100644 src/components/form/SortableList.tsx

diff --git a/package.json b/package.json
index b9482526..ad6a855b 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,9 @@
     ]
   },
   "dependencies": {
+    "@dnd-kit/core": "^6.1.0",
+    "@dnd-kit/sortable": "^8.0.0",
+    "@dnd-kit/utilities": "^3.2.2",
     "@formkit/auto-animate": "^0.8.1",
     "@headlessui/react": "^1.7.17",
     "@ladjs/country-language": "^1.0.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 54dd87b7..0bca4afe 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -12,6 +12,15 @@ overrides:
   rollup: npm:@rollup/wasm-node
 
 dependencies:
+  '@dnd-kit/core':
+    specifier: ^6.1.0
+    version: 6.1.0(react-dom@18.2.0)(react@18.2.0)
+  '@dnd-kit/sortable':
+    specifier: ^8.0.0
+    version: 8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0)
+  '@dnd-kit/utilities':
+    specifier: ^3.2.2
+    version: 3.2.2(react@18.2.0)
   '@formkit/auto-animate':
     specifier: ^0.8.1
     version: 0.8.1
@@ -274,7 +283,7 @@ devDependencies:
     version: 0.5.9(prettier@3.1.1)
   rollup-plugin-visualizer:
     specifier: ^5.11.0
-    version: 5.11.0(@rollup/wasm-node@4.14.2)
+    version: 5.11.0(@rollup/wasm-node@4.14.3)
   tailwind-scrollbar:
     specifier: ^3.0.5
     version: 3.0.5(tailwindcss@3.4.0)
@@ -2688,6 +2697,49 @@ packages:
       to-fast-properties: 2.0.0
     dev: true
 
+  /@dnd-kit/accessibility@3.1.0(react@18.2.0):
+    resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==}
+    peerDependencies:
+      react: '>=16.8.0'
+    dependencies:
+      react: 18.2.0
+      tslib: 2.6.2
+    dev: false
+
+  /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==}
+    peerDependencies:
+      react: '>=16.8.0'
+      react-dom: '>=16.8.0'
+    dependencies:
+      '@dnd-kit/accessibility': 3.1.0(react@18.2.0)
+      '@dnd-kit/utilities': 3.2.2(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+      tslib: 2.6.2
+    dev: false
+
+  /@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0)(react@18.2.0):
+    resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==}
+    peerDependencies:
+      '@dnd-kit/core': ^6.1.0
+      react: '>=16.8.0'
+    dependencies:
+      '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0)
+      '@dnd-kit/utilities': 3.2.2(react@18.2.0)
+      react: 18.2.0
+      tslib: 2.6.2
+    dev: false
+
+  /@dnd-kit/utilities@3.2.2(react@18.2.0):
+    resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
+    peerDependencies:
+      react: '>=16.8.0'
+    dependencies:
+      react: 18.2.0
+      tslib: 2.6.2
+    dev: false
+
   /@esbuild/aix-ppc64@0.19.10:
     resolution: {integrity: sha512-Q+mk96KJ+FZ30h9fsJl+67IjNJm3x2eX+GBWGmocAKgzp27cowCOOqSdscX80s0SpdFXZnIv/+1xD1EctFx96Q==}
     engines: {node: '>=12'}
@@ -3175,7 +3227,7 @@ packages:
     engines: {node: '>=14.0.0'}
     dev: false
 
-  /@rollup/plugin-babel@5.3.1(@babel/core@7.24.3)(@rollup/wasm-node@4.14.2):
+  /@rollup/plugin-babel@5.3.1(@babel/core@7.24.3)(@rollup/wasm-node@4.14.3):
     resolution: {integrity: sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==}
     engines: {node: '>= 10.0.0'}
     peerDependencies:
@@ -3188,36 +3240,36 @@ packages:
     dependencies:
       '@babel/core': 7.24.3
       '@babel/helper-module-imports': 7.24.3
-      '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.14.2)
-      rollup: /@rollup/wasm-node@4.14.2
+      '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.14.3)
+      rollup: /@rollup/wasm-node@4.14.3
     dev: true
 
-  /@rollup/plugin-node-resolve@11.2.1(@rollup/wasm-node@4.14.2):
+  /@rollup/plugin-node-resolve@11.2.1(@rollup/wasm-node@4.14.3):
     resolution: {integrity: sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==}
     engines: {node: '>= 10.0.0'}
     peerDependencies:
       rollup: npm:@rollup/wasm-node
     dependencies:
-      '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.14.2)
+      '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.14.3)
       '@types/resolve': 1.17.1
       builtin-modules: 3.3.0
       deepmerge: 4.3.1
       is-module: 1.0.0
       resolve: 1.22.8
-      rollup: /@rollup/wasm-node@4.14.2
+      rollup: /@rollup/wasm-node@4.14.3
     dev: true
 
-  /@rollup/plugin-replace@2.4.2(@rollup/wasm-node@4.14.2):
+  /@rollup/plugin-replace@2.4.2(@rollup/wasm-node@4.14.3):
     resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==}
     peerDependencies:
       rollup: npm:@rollup/wasm-node
     dependencies:
-      '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.14.2)
+      '@rollup/pluginutils': 3.1.0(@rollup/wasm-node@4.14.3)
       magic-string: 0.25.9
-      rollup: /@rollup/wasm-node@4.14.2
+      rollup: /@rollup/wasm-node@4.14.3
     dev: true
 
-  /@rollup/pluginutils@3.1.0(@rollup/wasm-node@4.14.2):
+  /@rollup/pluginutils@3.1.0(@rollup/wasm-node@4.14.3):
     resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
     engines: {node: '>= 8.0.0'}
     peerDependencies:
@@ -3226,11 +3278,11 @@ packages:
       '@types/estree': 0.0.39
       estree-walker: 1.0.1
       picomatch: 2.3.1
-      rollup: /@rollup/wasm-node@4.14.2
+      rollup: /@rollup/wasm-node@4.14.3
     dev: true
 
-  /@rollup/wasm-node@4.14.2:
-    resolution: {integrity: sha512-iwZbxtvP/0icwPWExUZWfA3A2jqQkDY38E8R5onRY2ALFmom0k7e37n9WDcJMMRcx/pdenfN8NaSohzX9LiDEQ==}
+  /@rollup/wasm-node@4.14.3:
+    resolution: {integrity: sha512-UyFUQV/iAu/Wt6rY6uQMYBQlfTMsynzYVIz6i7s9ySwjoG9WDNgtkK1TrazCSrUFbmuPZi2gbJm6VWdJCVw2yA==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
     dependencies:
@@ -6557,7 +6609,7 @@ packages:
       '@babel/plugin-syntax-typescript': 7.23.3(@babel/core@7.23.6)
       '@babel/types': 7.23.6
       kleur: 4.1.5
-      rollup: /@rollup/wasm-node@4.14.2
+      rollup: /@rollup/wasm-node@4.14.3
       unplugin: 1.5.1
     transitivePeerDependencies:
       - supports-color
@@ -7529,7 +7581,7 @@ packages:
       glob: 7.2.3
     dev: true
 
-  /rollup-plugin-terser@7.0.2(@rollup/wasm-node@4.14.2):
+  /rollup-plugin-terser@7.0.2(@rollup/wasm-node@4.14.3):
     resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==}
     deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser
     peerDependencies:
@@ -7537,12 +7589,12 @@ packages:
     dependencies:
       '@babel/code-frame': 7.24.2
       jest-worker: 26.6.2
-      rollup: /@rollup/wasm-node@4.14.2
+      rollup: /@rollup/wasm-node@4.14.3
       serialize-javascript: 4.0.0
       terser: 5.30.0
     dev: true
 
-  /rollup-plugin-visualizer@5.11.0(@rollup/wasm-node@4.14.2):
+  /rollup-plugin-visualizer@5.11.0(@rollup/wasm-node@4.14.3):
     resolution: {integrity: sha512-exM0Ms2SN3AgTzMeW7y46neZQcyLY7eKwWAop1ZoRTCZwyrIRdMMJ6JjToAJbML77X/9N8ZEpmXG4Z/Clb9k8g==}
     engines: {node: '>=14'}
     hasBin: true
@@ -7554,7 +7606,7 @@ packages:
     dependencies:
       open: 8.4.2
       picomatch: 2.3.1
-      rollup: /@rollup/wasm-node@4.14.2
+      rollup: /@rollup/wasm-node@4.14.3
       source-map: 0.7.4
       yargs: 17.7.2
     dev: true
@@ -8692,7 +8744,7 @@ packages:
       '@types/node': 20.10.5
       esbuild: 0.19.10
       postcss: 8.4.32
-      rollup: /@rollup/wasm-node@4.14.2
+      rollup: /@rollup/wasm-node@4.14.3
     optionalDependencies:
       fsevents: 2.3.3
     dev: true
@@ -8959,9 +9011,9 @@ packages:
       '@babel/core': 7.24.3
       '@babel/preset-env': 7.24.3(@babel/core@7.24.3)
       '@babel/runtime': 7.24.1
-      '@rollup/plugin-babel': 5.3.1(@babel/core@7.24.3)(@rollup/wasm-node@4.14.2)
-      '@rollup/plugin-node-resolve': 11.2.1(@rollup/wasm-node@4.14.2)
-      '@rollup/plugin-replace': 2.4.2(@rollup/wasm-node@4.14.2)
+      '@rollup/plugin-babel': 5.3.1(@babel/core@7.24.3)(@rollup/wasm-node@4.14.3)
+      '@rollup/plugin-node-resolve': 11.2.1(@rollup/wasm-node@4.14.3)
+      '@rollup/plugin-replace': 2.4.2(@rollup/wasm-node@4.14.3)
       '@surma/rollup-plugin-off-main-thread': 2.2.3
       ajv: 8.12.0
       common-tags: 1.8.2
@@ -8970,8 +9022,8 @@ packages:
       glob: 7.2.3
       lodash: 4.17.21
       pretty-bytes: 5.6.0
-      rollup: /@rollup/wasm-node@4.14.2
-      rollup-plugin-terser: 7.0.2(@rollup/wasm-node@4.14.2)
+      rollup: /@rollup/wasm-node@4.14.3
+      rollup-plugin-terser: 7.0.2(@rollup/wasm-node@4.14.3)
       source-map: 0.8.0-beta.0
       stringify-object: 3.3.0
       strip-comments: 2.0.1
@@ -9016,6 +9068,7 @@ packages:
 
   /workbox-google-analytics@7.0.0:
     resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
+    deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
     dependencies:
       workbox-background-sync: 7.0.0
       workbox-core: 7.0.0
diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json
index 6b951af6..acab2c56 100644
--- a/src/assets/locales/en.json
+++ b/src/assets/locales/en.json
@@ -527,6 +527,8 @@
       "autoplay": "Autoplay",
       "autoplayDescription": "Automatically play the next episode in a series after reaching the end. Can be enabled by users with the browser extension, a custom proxy, or with the default setup if allowed by the host.",
       "autoplayLabel": "Autoplay",
+      "sourceOrder": "Reordering sources",
+      "sourceOrderDescription": "Drag and drop to reorder sources. This will determine the order in which sources are checked for the media you are trying to watch.",
       "title": "Preferences"
     },
     "reset": "Reset",
diff --git a/src/components/form/SortableList.tsx b/src/components/form/SortableList.tsx
new file mode 100644
index 00000000..90c8fd94
--- /dev/null
+++ b/src/components/form/SortableList.tsx
@@ -0,0 +1,89 @@
+import {
+  DndContext,
+  DragEndEvent,
+  KeyboardSensor,
+  PointerSensor,
+  closestCenter,
+  useSensor,
+  useSensors,
+} from "@dnd-kit/core";
+import {
+  SortableContext,
+  arrayMove,
+  sortableKeyboardCoordinates,
+  useSortable,
+  verticalListSortingStrategy,
+} from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
+import classNames from "classnames";
+
+import { Icon, Icons } from "../Icon";
+
+function SortableItem(props: { id: string }) {
+  const { attributes, listeners, setNodeRef, transform, transition } =
+    useSortable({ id: props.id });
+
+  const style = {
+    transform: CSS.Transform.toString(transform),
+    transition,
+  };
+
+  return (
+    <div
+      ref={setNodeRef}
+      style={style}
+      {...attributes}
+      {...listeners}
+      className={classNames(
+        "bg-dropdown-background hover:bg-dropdown-hoverBackground select-none cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg",
+        transform && "cursor-grabbing",
+      )}
+    >
+      <span className="flex-1 text-white font-bold">{props.id}</span>
+      <Icon icon={Icons.MENU} />
+    </div>
+  );
+}
+
+export function DraggableList(props: {
+  items: string[];
+  setItems: (items: string[]) => void;
+}) {
+  const sensors = useSensors(
+    useSensor(PointerSensor),
+    useSensor(KeyboardSensor, {
+      coordinateGetter: sortableKeyboardCoordinates,
+    }),
+  );
+
+  const handleDragEnd = (event: DragEndEvent) => {
+    const { active, over } = event;
+    if (!over) return;
+    if (active.id !== over.id) {
+      const currentItems = props.items;
+      const oldIndex = currentItems.indexOf(active.id as string);
+      const newIndex = currentItems.indexOf(over.id as string);
+      const newItems = arrayMove(currentItems, oldIndex, newIndex);
+      props.setItems(newItems);
+    }
+  };
+
+  return (
+    <DndContext
+      sensors={sensors}
+      collisionDetection={closestCenter}
+      onDragEnd={handleDragEnd}
+    >
+      <SortableContext
+        items={props.items}
+        strategy={verticalListSortingStrategy}
+      >
+        <div className="flex flex-col gap-2">
+          {props.items.map((id) => (
+            <SortableItem key={id} id={id} />
+          ))}
+        </div>
+      </SortableContext>
+    </DndContext>
+  );
+}
diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx
index 932c8449..e83973e2 100644
--- a/src/hooks/useProviderScrape.tsx
+++ b/src/hooks/useProviderScrape.tsx
@@ -14,6 +14,7 @@ import {
 } from "@/backend/helpers/providerApi";
 import { getLoadbalancedProviderApiUrl } from "@/backend/providers/fetchers";
 import { getProviders } from "@/backend/providers/providers";
+import { usePreferencesStore } from "@/stores/preferences";
 
 export interface ScrapingItems {
   id: string;
@@ -156,6 +157,8 @@ export function useScrape() {
     startScrape,
   } = useBaseScrape();
 
+  const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder);
+
   const startScraping = useCallback(
     async (media: ScrapeMedia) => {
       const providerApiUrl = getLoadbalancedProviderApiUrl();
@@ -181,6 +184,7 @@ export function useScrape() {
       const providers = getProviders();
       const output = await providers.runAll({
         media,
+        sourceOrder: preferredSourceOrder,
         events: {
           init: initEvent,
           start: startEvent,
@@ -199,6 +203,7 @@ export function useScrape() {
       discoverEmbedsEvent,
       getResult,
       startScrape,
+      preferredSourceOrder,
     ],
   );
 
diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts
index eb8cd73f..667093fb 100644
--- a/src/hooks/useSettingsState.ts
+++ b/src/hooks/useSettingsState.ts
@@ -52,6 +52,7 @@ export function useSettingsState(
     | undefined,
   enableThumbnails: boolean,
   enableAutoplay: boolean,
+  sourceOrder: string[],
 ) {
   const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
     useDerived(proxyUrls);
@@ -91,6 +92,12 @@ export function useSettingsState(
     resetEnableAutoplay,
     enableAutoplayChanged,
   ] = useDerived(enableAutoplay);
+  const [
+    sourceOrderState,
+    setSourceOrderState,
+    resetSourceOrder,
+    sourceOrderChanged,
+  ] = useDerived(sourceOrder);
 
   function reset() {
     resetTheme();
@@ -103,6 +110,7 @@ export function useSettingsState(
     resetProfile();
     resetEnableThumbnails();
     resetEnableAutoplay();
+    resetSourceOrder();
   }
 
   const changed =
@@ -114,7 +122,8 @@ export function useSettingsState(
     proxyUrlsChanged ||
     profileChanged ||
     enableThumbnailsChanged ||
-    enableAutoplayChanged;
+    enableAutoplayChanged ||
+    sourceOrderChanged;
 
   return {
     reset,
@@ -164,5 +173,10 @@ export function useSettingsState(
       set: setEnableAutoplayState,
       changed: enableAutoplayChanged,
     },
+    sourceOrder: {
+      state: sourceOrderState,
+      set: setSourceOrderState,
+      changed: sourceOrderChanged,
+    },
   };
 }
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index 997fa3ed..720d55ea 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -11,6 +11,7 @@ import {
 import { getSessions, updateSession } from "@/backend/accounts/sessions";
 import { updateSettings } from "@/backend/accounts/settings";
 import { editUser } from "@/backend/accounts/user";
+import { getProviders } from "@/backend/providers/providers";
 import { Button } from "@/components/buttons/Button";
 import { WideContainer } from "@/components/layout/WideContainer";
 import { UserIcons } from "@/components/UserIcon";
@@ -125,6 +126,9 @@ export function SettingsPage() {
   const enableAutoplay = usePreferencesStore((s) => s.enableAutoplay);
   const setEnableAutoplay = usePreferencesStore((s) => s.setEnableAutoplay);
 
+  const sourceOrder = usePreferencesStore((s) => s.sourceOrder);
+  const setSourceOrder = usePreferencesStore((s) => s.setSourceOrder);
+
   const account = useAuthStore((s) => s.account);
   const updateProfile = useAuthStore((s) => s.setAccountProfile);
   const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
@@ -148,6 +152,7 @@ export function SettingsPage() {
     account?.profile,
     enableThumbnails,
     enableAutoplay,
+    sourceOrder,
   );
 
   useEffect(() => {
@@ -201,6 +206,7 @@ export function SettingsPage() {
 
     setEnableThumbnails(state.enableThumbnails.state);
     setEnableAutoplay(state.enableAutoplay.state);
+    setSourceOrder(state.sourceOrder.state);
     setAppLanguage(state.appLanguage.state);
     setTheme(state.theme.state);
     setSubStyling(state.subtitleStyling.state);
@@ -227,6 +233,7 @@ export function SettingsPage() {
     setEnableThumbnails,
     state,
     setEnableAutoplay,
+    setSourceOrder,
     setAppLanguage,
     setTheme,
     setSubStyling,
@@ -274,6 +281,14 @@ export function SettingsPage() {
             setEnableThumbnails={state.enableThumbnails.set}
             enableAutoplay={state.enableAutoplay.state}
             setEnableAutoplay={state.enableAutoplay.set}
+            sourceOrder={
+              state.sourceOrder.state.length > 0
+                ? state.sourceOrder.state
+                : getProviders()
+                    .listSources()
+                    .map((s) => s.id)
+            }
+            setSourceOrder={state.sourceOrder.set}
           />
         </div>
         <div id="settings-appearance" className="mt-48">
diff --git a/src/pages/parts/settings/PreferencesPart.tsx b/src/pages/parts/settings/PreferencesPart.tsx
index 71f9c5f8..2dab46b9 100644
--- a/src/pages/parts/settings/PreferencesPart.tsx
+++ b/src/pages/parts/settings/PreferencesPart.tsx
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
 import { Toggle } from "@/components/buttons/Toggle";
 import { FlagIcon } from "@/components/FlagIcon";
 import { Dropdown } from "@/components/form/Dropdown";
+import { DraggableList } from "@/components/form/SortableList";
 import { Heading1 } from "@/components/utils/Text";
 import { appLanguageOptions } from "@/setup/i18n";
 import { isAutoplayAllowed } from "@/utils/autoplay";
@@ -16,6 +17,8 @@ export function PreferencesPart(props: {
   setEnableThumbnails: (v: boolean) => void;
   enableAutoplay: boolean;
   setEnableAutoplay: (v: boolean) => void;
+  sourceOrder: string[];
+  setSourceOrder: (v: string[]) => void;
 }) {
   const { t } = useTranslation();
   const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
@@ -94,6 +97,20 @@ export function PreferencesPart(props: {
           </p>
         </div>
       </div>
+
+      <div className="flex flex-col gap-3">
+        <p className="text-white font-bold">
+          {t("settings.preferences.sourceOrder")}
+        </p>
+        <p className="max-w-[25rem] font-medium">
+          {t("settings.preferences.sourceOrderDescription")}
+        </p>
+
+        <DraggableList
+          items={props.sourceOrder}
+          setItems={props.setSourceOrder}
+        />
+      </div>
     </div>
   );
 }
diff --git a/src/stores/preferences/index.tsx b/src/stores/preferences/index.tsx
index ce198388..cc421573 100644
--- a/src/stores/preferences/index.tsx
+++ b/src/stores/preferences/index.tsx
@@ -4,26 +4,35 @@ import { immer } from "zustand/middleware/immer";
 
 export interface PreferencesStore {
   enableThumbnails: boolean;
-  setEnableThumbnails(v: boolean): void;
   enableAutoplay: boolean;
+  sourceOrder: string[];
+
+  setEnableThumbnails(v: boolean): void;
   setEnableAutoplay(v: boolean): void;
+  setSourceOrder(v: string[]): void;
 }
 
 export const usePreferencesStore = create(
   persist(
     immer<PreferencesStore>((set) => ({
       enableThumbnails: false,
+      enableAutoplay: false,
+      sourceOrder: [],
       setEnableThumbnails(v) {
         set((s) => {
           s.enableThumbnails = v;
         });
       },
-      enableAutoplay: false,
       setEnableAutoplay(v) {
         set((s) => {
           s.enableAutoplay = v;
         });
       },
+      setSourceOrder(v) {
+        set((s) => {
+          s.sourceOrder = v;
+        });
+      },
     })),
     {
       name: "__MW::preferences",