├── .eslintignore ├── .prettierignore ├── src ├── meridian │ ├── icons │ │ ├── index.ts │ │ ├── logo.png │ │ ├── romania.png │ │ ├── united-states.png │ │ └── types.ts │ ├── session │ │ ├── index.ts │ │ └── state │ │ │ ├── types.ts │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── reducer.ts │ │ │ └── actions.ts │ ├── state │ │ ├── index.ts │ │ ├── sagas │ │ │ ├── index.ts │ │ │ └── startupSaga.ts │ │ ├── types.ts │ │ └── store.ts │ ├── testing │ │ ├── index.ts │ │ └── helpers.tsx │ ├── torrentContent │ │ ├── index.ts │ │ └── state │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── reducer.ts │ │ │ └── saga.ts │ ├── torrentFilters │ │ ├── index.ts │ │ └── state │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── selectors.ts │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ ├── torrentTrackers │ │ ├── index.ts │ │ └── state │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── reducer.ts │ │ │ └── saga.ts │ ├── transformers │ │ ├── index.ts │ │ └── mainDataTransformer.ts │ ├── api │ │ └── index.ts │ ├── home │ │ ├── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useFetchTimer.ts │ │ │ ├── useManageSelection.ts │ │ │ ├── usePagination.ts │ │ │ ├── useFilteredTorrents.ts │ │ │ └── useHeaderMenuItems.tsx │ │ └── headerContent.tsx │ ├── login │ │ ├── index.ts │ │ ├── useNavigateToLogin.ts │ │ ├── useLoginForm.tsx │ │ └── loginPage.tsx │ ├── tags │ │ ├── index.ts │ │ ├── state │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── reducer.ts │ │ │ └── saga.ts │ │ ├── modals │ │ │ ├── index.ts │ │ │ ├── useCreateTagModal.tsx │ │ │ └── useTagsModal.tsx │ │ └── useTagsForm.ts │ ├── torrent │ │ ├── card │ │ │ ├── index.ts │ │ │ ├── fileInfo.tsx │ │ │ ├── __tests__ │ │ │ │ └── card.test.tsx │ │ │ ├── connectionInfo.tsx │ │ │ ├── progressIndicator.tsx │ │ │ ├── titleAndMenu.tsx │ │ │ ├── card.tsx │ │ │ └── statusInfo.tsx │ │ ├── index.ts │ │ ├── state │ │ │ ├── index.ts │ │ │ └── selectors.ts │ │ ├── modals │ │ │ ├── index.ts │ │ │ ├── useDeleteTorrentsModal.tsx │ │ │ ├── useTorrentCategoryModal.tsx │ │ │ └── useTorrentTagsModal.tsx │ │ ├── useAddTorrentForm.ts │ │ └── useContextMenuItems.tsx │ ├── categories │ │ ├── index.ts │ │ ├── state │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── reducer.ts │ │ │ └── saga.ts │ │ ├── modals │ │ │ ├── index.ts │ │ │ ├── useCreateCategoryModal.tsx │ │ │ ├── useEditCategoryModal.tsx │ │ │ └── useCategoriesModal.tsx │ │ └── useCategoryForm.ts │ ├── preferences │ │ ├── index.ts │ │ ├── modals │ │ │ ├── index.ts │ │ │ ├── sections │ │ │ │ ├── speed │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── globalRateLimitsSection.tsx │ │ │ │ │ ├── rateLimitsSettingsSection.tsx │ │ │ │ │ └── speedSection.tsx │ │ │ │ ├── webui │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── securitySection.tsx │ │ │ │ │ └── authenticationSection.tsx │ │ │ │ ├── bitTorrent │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── privacySection.tsx │ │ │ │ │ ├── bitTorrentSection.tsx │ │ │ │ │ └── seedingLimitsSection.tsx │ │ │ │ ├── connection │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── listeningPortSection.tsx │ │ │ │ │ ├── ipFilteringSection.tsx │ │ │ │ │ ├── connectionLimitsSection.tsx │ │ │ │ │ └── connectionSection.tsx │ │ │ │ ├── downloads │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── addingTorrentSection.tsx │ │ │ │ │ └── downloadsSection.tsx │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ └── usePreferencesModal.tsx │ │ └── state │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── reducer.ts │ │ │ └── saga.ts │ ├── settings │ │ ├── index.ts │ │ ├── modals │ │ │ ├── index.ts │ │ │ ├── useSettings.ts │ │ │ └── useSettingsModal.tsx │ │ └── state │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── selectors.ts │ │ │ ├── actions.ts │ │ │ └── reducer.ts │ ├── resource │ │ ├── baseAction.ts │ │ ├── index.ts │ │ ├── createSetResourceSaga.ts │ │ ├── createFetchResourceSaga.ts │ │ ├── createDeleteResourceSaga.ts │ │ └── types.ts │ ├── torrentProperties │ │ ├── index.ts │ │ ├── modals │ │ │ ├── index.ts │ │ │ ├── tabs │ │ │ │ ├── index.ts │ │ │ │ ├── generalTab.tsx │ │ │ │ ├── transferTab.tsx │ │ │ │ └── contentsTab.tsx │ │ │ └── useTorrentPropertiesModal.tsx │ │ └── state │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── reducer.ts │ │ │ └── saga.ts │ ├── mock │ │ ├── tags.ts │ │ ├── isMockEnabled.ts │ │ ├── index.ts │ │ ├── categories.ts │ │ ├── mainData.ts │ │ ├── preferences.ts │ │ └── transferInfo.ts │ ├── mainData │ │ ├── modals │ │ │ ├── index.ts │ │ │ └── useServerStateModal.tsx │ │ ├── index.ts │ │ ├── state │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── reducer.ts │ │ │ └── saga.ts │ │ └── hooks.ts │ ├── models │ │ ├── connectionSettings.ts │ │ ├── category.ts │ │ ├── session.ts │ │ ├── torrentFilters.ts │ │ ├── settings.ts │ │ ├── index.ts │ │ ├── peer.ts │ │ ├── mainData.ts │ │ ├── torrentContent.ts │ │ ├── torrentTracker.ts │ │ ├── sync.ts │ │ ├── torrentProperties.ts │ │ └── transferInfo.ts │ ├── navigation │ │ ├── types.ts │ │ ├── history.ts │ │ ├── index.ts │ │ └── router.tsx │ ├── transferInfo │ │ ├── index.ts │ │ ├── state │ │ │ ├── index.ts │ │ │ ├── selectors.ts │ │ │ ├── reducer.ts │ │ │ └── saga.ts │ │ └── card.tsx │ ├── snackbar │ │ ├── index.ts │ │ ├── actions.ts │ │ └── saga.tsx │ ├── localStorage │ │ ├── types.ts │ │ ├── index.ts │ │ ├── useLocalStorage.ts │ │ └── localStorage.ts │ ├── i18n │ │ ├── index.ts │ │ ├── types.ts │ │ ├── i18nProvider.tsx │ │ └── languagePicker.tsx │ ├── generic │ │ ├── __tests__ │ │ │ ├── __snapshots__ │ │ │ │ ├── scrollToTopAffix.test.tsx.snap │ │ │ │ ├── labelWithText.test.tsx.snap │ │ │ │ └── labelWithBadge.test.tsx.snap │ │ │ ├── labelWithText.test.tsx │ │ │ ├── labelWithBadge.test.tsx │ │ │ └── scrollToTopAffix.test.tsx │ │ ├── useIsSmallDevice.ts │ │ ├── commonConfigs.ts │ │ ├── useWindowSize.ts │ │ ├── index.ts │ │ ├── contextMenu.tsx │ │ ├── page.tsx │ │ ├── labelWithText.tsx │ │ ├── scrollToTopAffix.tsx │ │ ├── labelWithBadge.tsx │ │ ├── drawerPage.tsx │ │ ├── colorSchemeToggle.tsx │ │ └── dropzone.tsx │ ├── hooks │ │ ├── useIsLoggedIn.ts │ │ ├── index.ts │ │ ├── useLogout.ts │ │ ├── useLogin.ts │ │ ├── useCloseLastModal.ts │ │ ├── useCreateResource.ts │ │ ├── useFetchResource.ts │ │ ├── useDeleteResource.ts │ │ └── useToggleTheme.ts │ ├── importMetaUtils.ts │ ├── ThemeProvider.tsx │ ├── utils.ts │ └── useAboutModal.tsx ├── vite-env.d.ts ├── locales │ ├── en │ │ └── messages.d.ts │ └── ro │ │ └── messages.d.ts ├── main.tsx ├── index.css └── App.tsx ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png └── vite.svg ├── configs ├── mock.sh ├── release.sh └── debug.sh ├── .linguirc ├── tsconfig.node.json ├── .babelrc ├── .gitignore ├── setupTests.ts ├── index.html ├── vite.config.ts ├── .prettierrc ├── deploy.sh ├── tsconfig.json ├── .circleci └── config.yml ├── .eslintrc └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | *.test.* -------------------------------------------------------------------------------- /src/meridian/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | -------------------------------------------------------------------------------- /src/meridian/session/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | -------------------------------------------------------------------------------- /src/meridian/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './store'; 2 | -------------------------------------------------------------------------------- /src/meridian/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers'; 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/meridian/torrentContent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | -------------------------------------------------------------------------------- /src/meridian/torrentFilters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | -------------------------------------------------------------------------------- /src/meridian/torrentTrackers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | -------------------------------------------------------------------------------- /src/meridian/transformers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mainDataTransformer'; 2 | -------------------------------------------------------------------------------- /src/meridian/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/meridian/home/index.ts: -------------------------------------------------------------------------------- 1 | export { default as HomePage } from './homePage'; 2 | -------------------------------------------------------------------------------- /src/meridian/login/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LoginPage } from './loginPage'; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gabi1M/QBitUI/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/meridian/tags/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | export * from './modals'; 3 | -------------------------------------------------------------------------------- /src/meridian/torrent/card/index.ts: -------------------------------------------------------------------------------- 1 | export { default as TorrentCard } from './card'; 2 | -------------------------------------------------------------------------------- /src/meridian/categories/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | export * from './modals'; 3 | -------------------------------------------------------------------------------- /src/meridian/preferences/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | export * from './modals'; 3 | -------------------------------------------------------------------------------- /src/meridian/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | export * from './modals'; 3 | -------------------------------------------------------------------------------- /src/meridian/state/sagas/index.ts: -------------------------------------------------------------------------------- 1 | export { default as startupSaga } from './startupSaga'; 2 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gabi1M/QBitUI/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gabi1M/QBitUI/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /src/meridian/resource/baseAction.ts: -------------------------------------------------------------------------------- 1 | export interface BaseAction { 2 | type: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/meridian/torrentProperties/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | export * from './modals'; 3 | -------------------------------------------------------------------------------- /configs/mock.sh: -------------------------------------------------------------------------------- 1 | export VITE_MOCK_ENABLED="true" 2 | export VITE_API_URL="" 3 | export DEPLOY_PATH="" -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gabi1M/QBitUI/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/meridian/icons/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gabi1M/QBitUI/HEAD/src/meridian/icons/logo.png -------------------------------------------------------------------------------- /src/meridian/mock/tags.ts: -------------------------------------------------------------------------------- 1 | export const MockTags: string[] = ['Tag 1', 'Tag 2', 'Tag 3', 'Tag 4']; 2 | -------------------------------------------------------------------------------- /src/meridian/settings/modals/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useSettingsModal } from './useSettingsModal'; 2 | -------------------------------------------------------------------------------- /configs/release.sh: -------------------------------------------------------------------------------- 1 | export VITE_MOCK_ENABLED="false" 2 | export VITE_API_URL="" 3 | export DEPLOY_PATH="./release" -------------------------------------------------------------------------------- /src/meridian/icons/romania.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gabi1M/QBitUI/HEAD/src/meridian/icons/romania.png -------------------------------------------------------------------------------- /src/meridian/mainData/modals/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useServerStateModal } from './useServerStateModal'; 2 | -------------------------------------------------------------------------------- /src/meridian/models/connectionSettings.ts: -------------------------------------------------------------------------------- 1 | export interface ConnectionSettings { 2 | url: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/meridian/navigation/types.ts: -------------------------------------------------------------------------------- 1 | export enum AppRoutes { 2 | HOME = '/', 3 | LOGIN = 'login', 4 | } 5 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gabi1M/QBitUI/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gabi1M/QBitUI/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/meridian/mainData/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | export * from './hooks'; 3 | export * from './modals'; 4 | -------------------------------------------------------------------------------- /src/meridian/models/category.ts: -------------------------------------------------------------------------------- 1 | export interface Category { 2 | name: string; 3 | savePath: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/meridian/preferences/modals/index.ts: -------------------------------------------------------------------------------- 1 | export { default as usePreferencesModal } from './usePreferencesModal'; 2 | -------------------------------------------------------------------------------- /src/meridian/preferences/modals/sections/speed/index.ts: -------------------------------------------------------------------------------- 1 | export { default as SpeedSection } from './speedSection'; 2 | -------------------------------------------------------------------------------- /src/meridian/preferences/modals/sections/webui/index.ts: -------------------------------------------------------------------------------- 1 | export { default as WebUiSection } from './webUiSection'; 2 | -------------------------------------------------------------------------------- /src/meridian/icons/united-states.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gabi1M/QBitUI/HEAD/src/meridian/icons/united-states.png -------------------------------------------------------------------------------- /src/meridian/transferInfo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | export { default as TransferInfoCard } from './card'; 3 | -------------------------------------------------------------------------------- /configs/debug.sh: -------------------------------------------------------------------------------- 1 | export VITE_MOCK_ENABLED="false" 2 | export VITE_API_URL="http://192.168.0.189:8080" 3 | export DEPLOY_PATH="" -------------------------------------------------------------------------------- /src/meridian/preferences/modals/sections/bitTorrent/index.ts: -------------------------------------------------------------------------------- 1 | export { default as BitTorrentSection } from './bitTorrentSection'; 2 | -------------------------------------------------------------------------------- /src/meridian/preferences/modals/sections/connection/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ConnectionSection } from './connectionSection'; 2 | -------------------------------------------------------------------------------- /src/meridian/preferences/modals/sections/downloads/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DownloadsSection } from './downloadsSection'; 2 | -------------------------------------------------------------------------------- /src/meridian/settings/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './reducer'; 3 | export * from './selectors'; 4 | -------------------------------------------------------------------------------- /src/meridian/snackbar/index.ts: -------------------------------------------------------------------------------- 1 | export { default as snackbarSaga } from './saga'; 2 | export { showSnackbarAction } from './actions'; 3 | -------------------------------------------------------------------------------- /src/meridian/torrentFilters/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './reducer'; 3 | export * from './selectors'; 4 | -------------------------------------------------------------------------------- /src/meridian/torrentProperties/modals/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useTorrentPropertiesModal } from './useTorrentPropertiesModal'; 2 | -------------------------------------------------------------------------------- /src/locales/en/messages.d.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from '@lingui/core'; 2 | 3 | declare const messages: Messages; 4 | export { messages }; 5 | -------------------------------------------------------------------------------- /src/locales/ro/messages.d.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from '@lingui/core'; 2 | 3 | declare const messages: Messages; 4 | export { messages }; 5 | -------------------------------------------------------------------------------- /src/meridian/navigation/history.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | 3 | export const history = createBrowserHistory(); 4 | -------------------------------------------------------------------------------- /src/meridian/navigation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export { default as AppRouter } from './router'; 3 | export * from './history'; 4 | -------------------------------------------------------------------------------- /src/meridian/tags/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './selectors'; 3 | export { default as tagsSaga } from './saga'; 4 | -------------------------------------------------------------------------------- /src/meridian/torrent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './card'; 2 | export * from './modals'; 3 | export * from './state'; 4 | export * from './hooks'; 5 | -------------------------------------------------------------------------------- /src/meridian/localStorage/types.ts: -------------------------------------------------------------------------------- 1 | export enum LocalStorageKey { 2 | SETTINGS = 'settings', 3 | TORRENT_FILTERS = 'torrent_filters', 4 | } 5 | -------------------------------------------------------------------------------- /src/meridian/torrent/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './selectors'; 3 | export { default as torrentSaga } from './saga'; 4 | -------------------------------------------------------------------------------- /src/meridian/categories/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './selectors'; 3 | export { default as categoriesSaga } from './saga'; 4 | -------------------------------------------------------------------------------- /src/meridian/mainData/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './selectors'; 3 | export { default as mainDataSaga } from './saga'; 4 | -------------------------------------------------------------------------------- /src/meridian/mock/isMockEnabled.ts: -------------------------------------------------------------------------------- 1 | import { getIsMockEnabled } from 'meridian/importMetaUtils'; 2 | 3 | export const isMockEnabled = getIsMockEnabled(); 4 | -------------------------------------------------------------------------------- /src/meridian/session/state/types.ts: -------------------------------------------------------------------------------- 1 | export interface SessionState { 2 | loggedIn: boolean; 3 | version: string; 4 | apiVersion: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/meridian/preferences/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './selectors'; 3 | export { default as preferencesSaga } from './saga'; 4 | -------------------------------------------------------------------------------- /src/meridian/settings/state/types.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from 'meridian/models'; 2 | 3 | export interface SettingsState { 4 | settings: Settings; 5 | } 6 | -------------------------------------------------------------------------------- /src/meridian/transferInfo/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './selectors'; 3 | export { default as transferInfoSaga } from './saga'; 4 | -------------------------------------------------------------------------------- /src/meridian/tags/modals/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useCreateTagModal } from './useCreateTagModal'; 2 | export { default as useTagsModal } from './useTagsModal'; 3 | -------------------------------------------------------------------------------- /src/meridian/torrentContent/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './selectors'; 3 | export { default as torrentContentSaga } from './saga'; 4 | -------------------------------------------------------------------------------- /src/meridian/torrentTrackers/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './selectors'; 3 | export { default as torrentTrackersSaga } from './saga'; 4 | -------------------------------------------------------------------------------- /src/meridian/torrentProperties/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './reducer'; 2 | export * from './selectors'; 3 | export { default as torrentPropertiesSaga } from './saga'; 4 | -------------------------------------------------------------------------------- /src/meridian/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export { default as I18nProvider } from './i18nProvider'; 2 | export { default as LanguagePicker } from './languagePicker'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/meridian/session/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | export * from './reducer'; 3 | export * from './selectors'; 4 | export { default as sessionSaga } from './saga'; 5 | -------------------------------------------------------------------------------- /src/meridian/torrentFilters/state/types.ts: -------------------------------------------------------------------------------- 1 | import { TorrentFilters } from 'meridian/models'; 2 | 3 | export interface TorrentFiltersState { 4 | filters: TorrentFilters; 5 | } 6 | -------------------------------------------------------------------------------- /src/meridian/localStorage/index.ts: -------------------------------------------------------------------------------- 1 | export { default as LocalStorage } from './localStorage'; 2 | export { default as useLocalStorage } from './useLocalStorage'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /src/meridian/preferences/modals/sections/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connection'; 2 | export * from './speed'; 3 | export * from './downloads'; 4 | export * from './bitTorrent'; 5 | export * from './webui'; 6 | -------------------------------------------------------------------------------- /.linguirc: -------------------------------------------------------------------------------- 1 | { 2 | "locales": ["en", "ro"], 3 | "sourceLocale": "en", 4 | "catalogs": [{ 5 | "path": "src/locales/{locale}/messages", 6 | "include": ["src"] 7 | }], 8 | "format": "po" 9 | } -------------------------------------------------------------------------------- /src/meridian/generic/__tests__/__snapshots__/scrollToTopAffix.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ScrollToTopAffix should render as expected 1`] = ``; 4 | -------------------------------------------------------------------------------- /src/meridian/mainData/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useFetchResource } from 'meridian/hooks'; 2 | import { Resource } from 'meridian/resource'; 3 | 4 | export const useRefreshMainData = () => useFetchResource(Resource.MAIN_DATA); 5 | -------------------------------------------------------------------------------- /src/meridian/models/session.ts: -------------------------------------------------------------------------------- 1 | export interface LoginData { 2 | username: string; 3 | password: string; 4 | } 5 | 6 | export enum LoginResponse { 7 | SUCCESS = 'Ok.', 8 | FAIL = 'Fails.', 9 | } 10 | -------------------------------------------------------------------------------- /src/meridian/hooks/useIsLoggedIn.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | 3 | import { selectSessionIsLoggedIn } from 'meridian/session'; 4 | 5 | export const useIsLoggedIn = () => useSelector(selectSessionIsLoggedIn); 6 | -------------------------------------------------------------------------------- /src/meridian/tags/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectTags = (state: GlobalState) => state.tagsState.fetch.data; 5 | -------------------------------------------------------------------------------- /src/meridian/mainData/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectMainData = (state: GlobalState) => state.mainDataState.fetch.data; 5 | -------------------------------------------------------------------------------- /src/meridian/settings/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectSettings = (state: GlobalState) => state.settingsState.settings; 5 | -------------------------------------------------------------------------------- /src/meridian/tags/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Resource, createResourceReducer } from 'meridian/resource'; 2 | 3 | const { reducer, actions } = createResourceReducer(Resource.TAGS); 4 | export { reducer as tagsReducer, actions as TagsActions }; 5 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /src/meridian/generic/useIsSmallDevice.ts: -------------------------------------------------------------------------------- 1 | import useWindowSize from './useWindowSize'; 2 | 3 | const useIsSmallDevice = () => { 4 | const { width } = useWindowSize(); 5 | return width < 450; 6 | }; 7 | 8 | export default useIsSmallDevice; 9 | -------------------------------------------------------------------------------- /src/meridian/categories/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectCategories = (state: GlobalState) => state.categoriesState.fetch.data; 5 | -------------------------------------------------------------------------------- /src/meridian/preferences/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectPreferences = (state: GlobalState) => state.preferencesState.fetch.data; 5 | -------------------------------------------------------------------------------- /src/meridian/generic/commonConfigs.ts: -------------------------------------------------------------------------------- 1 | import { ModalProps } from '@mantine/core'; 2 | 3 | export const commonModalConfiguration: Partial = { 4 | centered: true, 5 | overlayBlur: 5, 6 | overflow: 'inside', 7 | radius: 'lg', 8 | }; 9 | -------------------------------------------------------------------------------- /src/meridian/mainData/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Resource, createResourceReducer } from 'meridian/resource'; 2 | 3 | const { reducer, actions } = createResourceReducer(Resource.MAIN_DATA); 4 | export { reducer as mainDataReducer, actions as MainDataActions }; 5 | -------------------------------------------------------------------------------- /src/meridian/mock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './categories'; 2 | export * from './tags'; 3 | export * from './torrents'; 4 | export * from './transferInfo'; 5 | export * from './mainData'; 6 | export * from './preferences'; 7 | export * from './isMockEnabled'; 8 | -------------------------------------------------------------------------------- /src/meridian/models/torrentFilters.ts: -------------------------------------------------------------------------------- 1 | import { TorrentStateDescription } from './torrent'; 2 | 3 | export interface TorrentFilters { 4 | name: string; 5 | states: TorrentStateDescription[]; 6 | categories: string[]; 7 | tags: string[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/meridian/torrentFilters/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectTorrentFilters = (state: GlobalState) => state.torrentFiltersState.filters; 5 | -------------------------------------------------------------------------------- /src/meridian/transferInfo/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectTransferInfo = (state: GlobalState) => state.transferInfoState.fetch.data; 5 | -------------------------------------------------------------------------------- /src/meridian/torrentContent/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectTorrentContent = (state: GlobalState) => state.torrentContentState.fetch.data; 5 | -------------------------------------------------------------------------------- /src/meridian/categories/modals/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useCreateCategoryModal } from './useCreateCategoryModal'; 2 | export { default as useEditCategoryModal } from './useEditCategoryModal'; 3 | export { default as useCategoriesModal } from './useCategoriesModal'; 4 | -------------------------------------------------------------------------------- /src/meridian/categories/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Resource, createResourceReducer } from 'meridian/resource'; 2 | 3 | const { reducer, actions } = createResourceReducer(Resource.CATEGORIES); 4 | export { reducer as categoriesReducer, actions as CategoriesActions }; 5 | -------------------------------------------------------------------------------- /src/meridian/torrentTrackers/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectTorrentTrackers = (state: GlobalState) => state.torrentTrackersState.fetch.data; 5 | -------------------------------------------------------------------------------- /src/meridian/preferences/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Resource, createResourceReducer } from 'meridian/resource'; 2 | 3 | const { reducer, actions } = createResourceReducer(Resource.PREFERENCES); 4 | export { reducer as preferencesReducer, actions as PreferencesActions }; 5 | -------------------------------------------------------------------------------- /src/meridian/resource/index.ts: -------------------------------------------------------------------------------- 1 | export * from './baseAction'; 2 | export * from './types'; 3 | export * from './createResourceReducer'; 4 | export * from './createFetchResourceSaga'; 5 | export * from './createSetResourceSaga'; 6 | export * from './createDeleteResourceSaga'; 7 | -------------------------------------------------------------------------------- /src/meridian/torrentProperties/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectTorrentProperties = (state: GlobalState) => 5 | state.torrentPropertiesState.fetch.data; 6 | -------------------------------------------------------------------------------- /src/meridian/transferInfo/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Resource, createResourceReducer } from 'meridian/resource'; 2 | 3 | const { reducer, actions } = createResourceReducer(Resource.TRANSFER_INFO); 4 | export { reducer as transferInfoReducer, actions as TransferInfoActions }; 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-typescript", 4 | [ 5 | "@babel/preset-react", 6 | { 7 | "runtime": "automatic", 8 | "importSource": "react" 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "macros" 14 | ] 15 | } -------------------------------------------------------------------------------- /src/meridian/torrentContent/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Resource, createResourceReducer } from 'meridian/resource'; 2 | 3 | const { reducer, actions } = createResourceReducer(Resource.TORRENT_CONTENT); 4 | export { reducer as torrentContentReducer, actions as TorrentContentActions }; 5 | -------------------------------------------------------------------------------- /src/meridian/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useFetchResource'; 2 | export * from './useCreateResource'; 3 | export * from './useDeleteResource'; 4 | export * from './useToggleTheme'; 5 | export * from './useLogin'; 6 | export * from './useLogout'; 7 | export * from './useCloseLastModal'; 8 | -------------------------------------------------------------------------------- /src/meridian/torrentProperties/modals/tabs/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GeneralTab } from './generalTab'; 2 | export { default as TransferTab } from './transferTab'; 3 | export { default as ContentsTab } from './contentsTab'; 4 | export { default as TrackersTab } from './trackersTab'; 5 | -------------------------------------------------------------------------------- /src/meridian/torrentTrackers/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Resource, createResourceReducer } from 'meridian/resource'; 2 | 3 | const { reducer, actions } = createResourceReducer(Resource.TORRENT_TRACKERS); 4 | export { reducer as torrentTrackersReducer, actions as TorrentTrackersActions }; 5 | -------------------------------------------------------------------------------- /src/meridian/icons/types.ts: -------------------------------------------------------------------------------- 1 | import logo from './logo.png'; 2 | import romania from './romania.png'; 3 | import unitedStates from './united-states.png'; 4 | 5 | export const Icons = Object.freeze({ 6 | ROMANIA: romania, 7 | UNITED_STATES: unitedStates, 8 | LOGO: logo, 9 | }); 10 | -------------------------------------------------------------------------------- /src/meridian/torrentProperties/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Resource, createResourceReducer } from 'meridian/resource'; 2 | 3 | const { reducer, actions } = createResourceReducer(Resource.TORRENT_PROPERTIES); 4 | export { reducer as torrentPropertiesReducer, actions as TorrentPropertiesActions }; 5 | -------------------------------------------------------------------------------- /src/meridian/importMetaUtils.ts: -------------------------------------------------------------------------------- 1 | export const getVersion = () => import.meta.env.VITE_VERSION; 2 | export const getIsDevEnv = () => import.meta.env.DEV; 3 | export const getIsMockEnabled = () => import.meta.env.VITE_MOCK_ENABLED === 'true'; 4 | export const getApiUrl = () => import.meta.env.VITE_API_URL; 5 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | 4 | import App from './App'; 5 | import './index.css'; 6 | 7 | createRoot(document.getElementById('root') as HTMLElement).render( 8 | 9 | 10 | , 11 | ); 12 | -------------------------------------------------------------------------------- /src/meridian/torrent/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectTorrents = (state: GlobalState) => state.torrentState.fetch.data; 5 | export const selectTorrentError = (state: GlobalState) => state.torrentState.fetch.error; 6 | -------------------------------------------------------------------------------- /src/meridian/models/settings.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { Language } from 'meridian/i18n/types'; 3 | 4 | export interface Settings { 5 | darkMode: boolean; 6 | autoRefresh: boolean; 7 | autoRefreshInterval: number; 8 | torrentsPerPage: number; 9 | language: Language; 10 | } 11 | -------------------------------------------------------------------------------- /src/meridian/torrent/modals/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useAddTorrentsModal } from './useAddTorrentsModal'; 2 | export { default as useDeleteTorrentsModal } from './useDeleteTorrentsModal'; 3 | export { default as useTorrentCategoryModal } from './useTorrentCategoryModal'; 4 | export { default as useTorrentTagsModal } from './useTorrentTagsModal'; 5 | -------------------------------------------------------------------------------- /src/meridian/login/useNavigateToLogin.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | import { AppRoutes } from 'meridian/navigation'; 5 | 6 | export const useNavigateToLogin = () => { 7 | const navigate = useNavigate(); 8 | return useCallback(() => navigate(AppRoutes.LOGIN), [navigate]); 9 | }; 10 | -------------------------------------------------------------------------------- /src/meridian/home/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useFetchTimer } from './useFetchTimer'; 2 | export { default as useFilteredTorrents } from './useFilteredTorrents'; 3 | export { default as useManageSelection } from './useManageSelection'; 4 | export { default as usePagination } from './usePagination'; 5 | export { default as useHeaderMenuItems } from './useHeaderMenuItems'; 6 | -------------------------------------------------------------------------------- /src/meridian/hooks/useLogout.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { logoutAction } from 'meridian/session'; 5 | 6 | export const useLogout = () => { 7 | const dispatch = useDispatch(); 8 | 9 | return useCallback(() => { 10 | dispatch(logoutAction()); 11 | }, [dispatch]); 12 | }; 13 | -------------------------------------------------------------------------------- /src/meridian/mainData/state/saga.ts: -------------------------------------------------------------------------------- 1 | import { takeLatest } from 'redux-saga/effects'; 2 | 3 | import { Resource, createFetchResourceSaga } from 'meridian/resource'; 4 | 5 | import { MainDataActions } from './reducer'; 6 | 7 | function* mainDataSaga() { 8 | yield takeLatest(MainDataActions.FETCH, createFetchResourceSaga(Resource.MAIN_DATA)); 9 | } 10 | 11 | export default mainDataSaga; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | coverage 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | release -------------------------------------------------------------------------------- /src/meridian/session/state/selectors.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable-next-line no-restricted-imports */ 2 | import { GlobalState } from 'meridian/state/types'; 3 | 4 | export const selectSessionIsLoggedIn = (state: GlobalState) => state.sessionState.loggedIn; 5 | 6 | export const selectVersions = (state: GlobalState) => ({ 7 | version: state.sessionState.version, 8 | apiVersion: state.sessionState.apiVersion, 9 | }); 10 | -------------------------------------------------------------------------------- /src/meridian/transferInfo/state/saga.ts: -------------------------------------------------------------------------------- 1 | import { takeLatest } from 'redux-saga/effects'; 2 | 3 | import { Resource, createFetchResourceSaga } from 'meridian/resource'; 4 | 5 | import { TransferInfoActions } from './reducer'; 6 | 7 | function* transferInfoSaga() { 8 | yield takeLatest(TransferInfoActions.FETCH, createFetchResourceSaga(Resource.TRANSFER_INFO)); 9 | } 10 | 11 | export default transferInfoSaga; 12 | -------------------------------------------------------------------------------- /src/meridian/transformers/mainDataTransformer.ts: -------------------------------------------------------------------------------- 1 | import { MainData, TorrentInfo } from 'meridian/models'; 2 | 3 | export const transformMainData = (mainData: MainData) => { 4 | mainData.torrents = Object.entries(mainData.torrents).reduce( 5 | (result, [hash, torrent]) => ((result[hash] = { ...torrent, hash }), result), 6 | {} as Record, 7 | ); 8 | return mainData; 9 | }; 10 | -------------------------------------------------------------------------------- /src/meridian/localStorage/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import LocalStorage from './localStorage'; 2 | import { LocalStorageKey } from './types'; 3 | 4 | const useLocalStorage = () => { 5 | const setValue = (key: LocalStorageKey, value: T) => LocalStorage.setValue(key, value); 6 | const getValue = (key: LocalStorageKey) => LocalStorage.getValue(key); 7 | return { setValue, getValue }; 8 | }; 9 | 10 | export default useLocalStorage; 11 | -------------------------------------------------------------------------------- /src/meridian/generic/__tests__/labelWithText.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { render } from '@testing-library/react'; 4 | 5 | import LabelWithText from '../labelWithText'; 6 | 7 | describe('LabelWithText', () => { 8 | it('should render as expected', () => { 9 | const result = render(); 10 | expect(result.asFragment()).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/meridian/hooks/useLogin.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { loginAction } from 'meridian/session'; 5 | 6 | export const useLogin = () => { 7 | const dispatch = useDispatch(); 8 | 9 | return useCallback( 10 | (username: string, password: string) => { 11 | dispatch(loginAction(username, password)); 12 | }, 13 | [dispatch], 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/meridian/mock/categories.ts: -------------------------------------------------------------------------------- 1 | import { Category } from 'meridian/models'; 2 | 3 | export const MockCategories: Record = { 4 | Category1: { 5 | name: 'Category 1', 6 | savePath: 'Save path 1', 7 | }, 8 | 'Category 2': { 9 | name: 'Category 2', 10 | savePath: 'Save path 2', 11 | }, 12 | 'Category 3': { 13 | name: 'Category 3', 14 | savePath: 'Save path 3', 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /src/meridian/torrentContent/state/saga.ts: -------------------------------------------------------------------------------- 1 | import { takeLatest } from 'redux-saga/effects'; 2 | 3 | import { Resource, createFetchResourceSaga } from 'meridian/resource'; 4 | 5 | import { TorrentContentActions } from './reducer'; 6 | 7 | function* torrentContentSaga() { 8 | yield takeLatest( 9 | TorrentContentActions.FETCH, 10 | createFetchResourceSaga(Resource.TORRENT_CONTENT), 11 | ); 12 | } 13 | 14 | export default torrentContentSaga; 15 | -------------------------------------------------------------------------------- /src/meridian/i18n/types.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/macro'; 2 | 3 | import { Icons } from 'meridian/icons'; 4 | 5 | export enum Language { 6 | ENGLISH = 'en', 7 | ROMANIAN = 'ro', 8 | } 9 | 10 | export const LanguageName = { 11 | [Language.ENGLISH]: t`English`, 12 | [Language.ROMANIAN]: t`Romanian`, 13 | }; 14 | 15 | export const LanguageIcon = { 16 | [Language.ENGLISH]: Icons.UNITED_STATES, 17 | [Language.ROMANIAN]: Icons.ROMANIA, 18 | }; 19 | -------------------------------------------------------------------------------- /src/meridian/torrentTrackers/state/saga.ts: -------------------------------------------------------------------------------- 1 | import { takeLatest } from 'redux-saga/effects'; 2 | 3 | import { Resource, createFetchResourceSaga } from 'meridian/resource'; 4 | 5 | import { TorrentTrackersActions } from './reducer'; 6 | 7 | function* torrentTrackersSaga() { 8 | yield takeLatest( 9 | TorrentTrackersActions.FETCH, 10 | createFetchResourceSaga(Resource.TORRENT_TRACKERS), 11 | ); 12 | } 13 | 14 | export default torrentTrackersSaga; 15 | -------------------------------------------------------------------------------- /src/meridian/generic/__tests__/labelWithBadge.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { render } from '@testing-library/react'; 4 | 5 | import LabelWithBadge from '../labelWithBadge'; 6 | 7 | describe('LabelWithBadge', () => { 8 | it('should render as expected', () => { 9 | const result = render(); 10 | expect(result.asFragment()).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /src/meridian/settings/state/actions.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from 'meridian/models'; 2 | import { BaseAction } from 'meridian/resource'; 3 | 4 | export enum SettingsActions { 5 | SET_SETTINGS = 'SET_SETTINGS', 6 | } 7 | 8 | export interface SetSettingsAction extends BaseAction { 9 | settings: Settings; 10 | } 11 | export const createSetSettingsAction = (settings: Settings): SetSettingsAction => ({ 12 | type: SettingsActions.SET_SETTINGS, 13 | settings, 14 | }); 15 | -------------------------------------------------------------------------------- /src/meridian/localStorage/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { LocalStorageKey } from './types'; 2 | 3 | class LocalStorage { 4 | static setValue = (key: LocalStorageKey, value: T) => { 5 | localStorage.setItem(key, JSON.stringify(value)); 6 | }; 7 | 8 | static getValue = (key: LocalStorageKey) => { 9 | const value = localStorage.getItem(key); 10 | return value ? (JSON.parse(value) as T) : null; 11 | }; 12 | } 13 | 14 | export default LocalStorage; 15 | -------------------------------------------------------------------------------- /src/meridian/torrentProperties/state/saga.ts: -------------------------------------------------------------------------------- 1 | import { takeLatest } from 'redux-saga/effects'; 2 | 3 | import { Resource, createFetchResourceSaga } from 'meridian/resource'; 4 | 5 | import { TorrentPropertiesActions } from './reducer'; 6 | 7 | function* torrentPropertiesSaga() { 8 | yield takeLatest( 9 | TorrentPropertiesActions.FETCH, 10 | createFetchResourceSaga(Resource.TORRENT_PROPERTIES), 11 | ); 12 | } 13 | 14 | export default torrentPropertiesSaga; 15 | -------------------------------------------------------------------------------- /setupTests.ts: -------------------------------------------------------------------------------- 1 | import fetchMock from 'jest-fetch-mock'; 2 | 3 | fetchMock.enableMocks(); 4 | 5 | jest.mock('@lingui/macro', () => { 6 | return { 7 | ...jest.requireActual('@lingui/macro'), 8 | t: jest.fn(), 9 | }; 10 | }); 11 | 12 | jest.mock('src/meridian/importMetaUtils', () => ({ 13 | getVersion: jest.fn(), 14 | getIsDevEnv: jest.fn(), 15 | getIsMockEnabled: jest.fn(), 16 | getApiUrl: jest.fn(() => 'http://127.0.0.1:1000'), 17 | })); 18 | 19 | export { } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | QBitUI 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/meridian/tags/useTagsForm.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { t } from '@lingui/macro'; 4 | 5 | import { useForm } from '@mantine/form'; 6 | 7 | const useTagForm = () => { 8 | const initialValues = { 9 | tagName: '', 10 | }; 11 | 12 | return useForm({ 13 | initialValues, 14 | validate: { 15 | tagName: (value) => (value !== '' ? null : t`Name cannot be empty`), 16 | }, 17 | }); 18 | }; 19 | 20 | export default useTagForm; 21 | -------------------------------------------------------------------------------- /src/meridian/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connectionSettings'; 2 | export * from './preferences'; 3 | export * from './transferInfo'; 4 | export * from './torrent'; 5 | export * from './torrentFilters'; 6 | export * from './category'; 7 | export * from './mainData'; 8 | export * from './peer'; 9 | export * from './sync'; 10 | export * from './session'; 11 | export * from './settings'; 12 | export * from './torrentProperties'; 13 | export * from './torrentContent'; 14 | export * from './torrentTracker'; 15 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 4 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | min-height: 100vh; 8 | } 9 | 10 | body > #root { 11 | min-height: 100vh; 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | 16 | ::-webkit-scrollbar { 17 | width: 0px; 18 | } 19 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react({ 8 | babel: { 9 | // Use .babelrc files, necessary to use LinguiJS CLI 10 | babelrc: true 11 | }, 12 | }), tsconfigPaths()], 13 | build: { 14 | chunkSizeWarningLimit: 1000, 15 | }, 16 | server: { 17 | host: '127.0.0.1', 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /src/meridian/hooks/useCloseLastModal.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | 3 | import { useModals } from '@mantine/modals'; 4 | 5 | export const useCloseLastModal = () => { 6 | const modals = useModals(); 7 | 8 | return useCallback(() => { 9 | const lastModal = modals.modals.length 10 | ? modals.modals[modals.modals.length - 1] 11 | : undefined; 12 | if (lastModal) { 13 | modals.closeModal(lastModal.id); 14 | } 15 | }, [modals]); 16 | }; 17 | -------------------------------------------------------------------------------- /src/meridian/models/peer.ts: -------------------------------------------------------------------------------- 1 | export interface Peer { 2 | client: string; 3 | connection: string; 4 | country: string; 5 | country_code: string; 6 | dl_speed: number; 7 | downloaded: number; 8 | up_speed: number; 9 | uploaded: number; 10 | files: string; 11 | flags: string; 12 | flags_desc: string; 13 | ip: string; 14 | port: number; 15 | progress: number; 16 | relevance: number; 17 | } 18 | 19 | export interface Peers { 20 | [ip_and_port: string]: Peer; 21 | } 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxSingleQuote": true, 3 | "singleQuote": true, 4 | "semi": true, 5 | "tabWidth": 4, 6 | "trailingComma": "all", 7 | "printWidth": 100, 8 | "bracketSameLine": false, 9 | "useTabs": false, 10 | "arrowParens": "always", 11 | "endOfLine": "auto", 12 | "importOrder": [ 13 | "^react.*$", 14 | "", 15 | "^@mantine.*$", 16 | "^meridian/(.*)$", 17 | "^../.*$", 18 | "^./.*$" 19 | ], 20 | "importOrderSeparation": true, 21 | "importOrderSortSpecifiers": true 22 | } -------------------------------------------------------------------------------- /src/meridian/preferences/modals/sections/types.ts: -------------------------------------------------------------------------------- 1 | import { Preferences } from 'meridian/models'; 2 | 3 | export interface SectionProps { 4 | preferences: Partial | undefined; 5 | updatePreferencesKey: ( 6 | name: keyof Preferences, 7 | value: string | boolean | number | string[], 8 | ) => void; 9 | updateBulkPreferencesKey: ( 10 | items: { 11 | name: keyof Preferences; 12 | value: string | boolean | number | string[]; 13 | }[], 14 | ) => void; 15 | } 16 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd . 4 | source configs/release.sh 5 | if [[ -z "${DEPLOY_PATH}" ]] 6 | then 7 | echo "Deploy path not set. Please set DEPLOY_PATH env var to desired path" 8 | exit 9 | else 10 | read -p "Deploying to ${DEPLOY_PATH}. Continue? (Y/N): " confirm 11 | if [[ $confirm == [yY] ]]; 12 | then 13 | rm -rf dist/ 14 | yarn build 15 | rm -rf ${DEPLOY_PATH} 16 | mkdir -p ${DEPLOY_PATH} 17 | cp -r dist/* ${DEPLOY_PATH} 18 | else 19 | exit 20 | fi 21 | fi -------------------------------------------------------------------------------- /src/meridian/torrentFilters/state/actions.ts: -------------------------------------------------------------------------------- 1 | import { TorrentFilters } from 'meridian/models'; 2 | import { BaseAction } from 'meridian/resource'; 3 | 4 | export enum TorrentFiltersActions { 5 | SET_TORRENT_FILTERS = 'SET_TORRENT_FILTERS', 6 | } 7 | 8 | export interface SetTorrentFiltersAction extends BaseAction { 9 | filters: TorrentFilters; 10 | } 11 | export const createSetTorrentFiltersAction = ( 12 | filters: TorrentFilters, 13 | ): SetTorrentFiltersAction => ({ 14 | type: TorrentFiltersActions.SET_TORRENT_FILTERS, 15 | filters, 16 | }); 17 | -------------------------------------------------------------------------------- /src/meridian/hooks/useCreateResource.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { Resource, SetResourceParams, createResourceSetAction } from 'meridian/resource'; 5 | 6 | export const useCreateResource = (resourceName: T) => { 7 | const dispatch = useDispatch(); 8 | 9 | return useCallback( 10 | (params: SetResourceParams[T]) => { 11 | dispatch(createResourceSetAction(resourceName, params)); 12 | }, 13 | [dispatch, resourceName], 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/meridian/hooks/useFetchResource.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { FetchResourceParams, Resource, createResourceFetchAction } from 'meridian/resource'; 5 | 6 | export const useFetchResource = (resourceName: T) => { 7 | const dispatch = useDispatch(); 8 | 9 | return useCallback( 10 | (params?: FetchResourceParams[T]) => { 11 | dispatch(createResourceFetchAction(resourceName, params)); 12 | }, 13 | [dispatch, resourceName], 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/meridian/torrent/useAddTorrentForm.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useForm } from '@mantine/form'; 4 | 5 | import { AddTorrentsParams } from 'meridian/api'; 6 | 7 | const useAddTorrentForm = () => { 8 | const initialValues: AddTorrentsParams = { 9 | urls: [], 10 | tags: [], 11 | torrents: [], 12 | paused: false, 13 | skipChecking: false, 14 | rootFolder: true, 15 | autoTMM: true, 16 | }; 17 | 18 | return useForm({ 19 | initialValues, 20 | }); 21 | }; 22 | 23 | export default useAddTorrentForm; 24 | -------------------------------------------------------------------------------- /src/meridian/hooks/useDeleteResource.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | 4 | import { DeleteResourceParams, Resource, createResourceDeleteAction } from 'meridian/resource'; 5 | 6 | export const useDeleteResource = (resourceName: T) => { 7 | const dispatch = useDispatch(); 8 | 9 | return useCallback( 10 | (params: DeleteResourceParams[T]) => { 11 | dispatch(createResourceDeleteAction(resourceName, params)); 12 | }, 13 | [dispatch, resourceName], 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/meridian/generic/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | export interface WindowSize { 4 | height: number; 5 | width: number; 6 | } 7 | 8 | const useWindowSize = () => { 9 | const [size, setSize] = useState({ 10 | height: window.innerHeight, 11 | width: window.innerWidth, 12 | }); 13 | 14 | useEffect(() => { 15 | window.addEventListener('resize', () => { 16 | setSize({ height: window.innerHeight, width: window.innerWidth }); 17 | }); 18 | }, []); 19 | 20 | return size; 21 | }; 22 | 23 | export default useWindowSize; 24 | -------------------------------------------------------------------------------- /src/meridian/models/mainData.ts: -------------------------------------------------------------------------------- 1 | import { Category } from './category'; 2 | import { TorrentInfo } from './torrent'; 3 | import { TransferInfo } from './transferInfo'; 4 | 5 | export interface MainData { 6 | rid: number; 7 | full_update: boolean; 8 | torrents: { 9 | [hash: string]: TorrentInfo; 10 | }; 11 | torrents_removed: string[]; 12 | categories: { 13 | [name: string]: Category; 14 | }; 15 | categories_removed: string[]; 16 | tags: string[]; 17 | tags_removed: string[]; 18 | server_state: TransferInfo; 19 | trackers: { 20 | [url: string]: string[]; 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/meridian/snackbar/actions.ts: -------------------------------------------------------------------------------- 1 | export enum SnackbarAction { 2 | SHOW_SNACKBAR = 'SHOW_SNACKBAR', 3 | } 4 | 5 | export type SnackbarVariant = 'default' | 'error' | 'info' | 'success' | 'warning'; 6 | 7 | export interface ShowSnackbarAction { 8 | type: SnackbarAction; 9 | text: string; 10 | variant: SnackbarVariant; 11 | autoHideDuration?: number; 12 | } 13 | 14 | export const showSnackbarAction = ( 15 | text: string, 16 | variant: SnackbarVariant, 17 | autoHideDuration?: number, 18 | ): ShowSnackbarAction => ({ 19 | type: SnackbarAction.SHOW_SNACKBAR, 20 | text, 21 | variant, 22 | autoHideDuration, 23 | }); 24 | -------------------------------------------------------------------------------- /src/meridian/login/useLoginForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { t } from '@lingui/macro'; 4 | 5 | import { useForm } from '@mantine/form'; 6 | 7 | import { LoginData } from 'meridian/models'; 8 | 9 | const useLoginForm = () => { 10 | const initialValues: LoginData = { 11 | username: '', 12 | password: '', 13 | }; 14 | 15 | return useForm({ 16 | initialValues, 17 | validate: { 18 | username: (value) => (value !== '' ? null : t`Username cannot be empty`), 19 | password: (value) => (value !== '' ? null : t`Password cannot be empty`), 20 | }, 21 | }); 22 | }; 23 | 24 | export default useLoginForm; 25 | -------------------------------------------------------------------------------- /src/meridian/models/torrentContent.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/macro'; 2 | 3 | export interface TorrentContent { 4 | index: string; 5 | name: string; 6 | size: number; 7 | progress: number; 8 | priority: FilePriority; 9 | is_seed: boolean; 10 | piece_range: number[]; 11 | availability: number; 12 | } 13 | 14 | export enum FilePriority { 15 | DO_NOT_DOWNLOAD = 0, 16 | NORMAL = 1, 17 | HIGH = 6, 18 | MAXIMUM = 7, 19 | } 20 | 21 | export const FilePriorityDescription = { 22 | [FilePriority.DO_NOT_DOWNLOAD]: t`Ignored`, 23 | [FilePriority.NORMAL]: t`Normal`, 24 | [FilePriority.HIGH]: t`High`, 25 | [FilePriority.MAXIMUM]: t`Maximum`, 26 | }; 27 | -------------------------------------------------------------------------------- /src/meridian/categories/useCategoryForm.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { t } from '@lingui/macro'; 4 | 5 | import { useForm } from '@mantine/form'; 6 | 7 | import { Category } from 'meridian/models'; 8 | 9 | const useCategoryForm = (category?: Category) => { 10 | const initialValues = category ?? { 11 | name: '', 12 | savePath: '', 13 | }; 14 | 15 | return useForm({ 16 | initialValues, 17 | validate: { 18 | name: (value) => (value !== '' ? null : t`Name cannot be empty`), 19 | savePath: (value) => (value !== '' ? null : t`Save path cannot be empty`), 20 | }, 21 | }); 22 | }; 23 | 24 | export default useCategoryForm; 25 | -------------------------------------------------------------------------------- /src/meridian/torrent/card/fileInfo.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { t } from '@lingui/macro'; 4 | 5 | import { Group, MantineStyleSystemProps } from '@mantine/core'; 6 | 7 | import { LabelWithText } from 'meridian/generic'; 8 | import { bytesToSize } from 'meridian/utils'; 9 | 10 | interface Props extends MantineStyleSystemProps { 11 | savePath: string; 12 | size: number; 13 | } 14 | 15 | const FileInfo = ({ savePath, size, ...props }: Props) => ( 16 | 17 | 18 | 19 | 20 | ); 21 | 22 | export default memo(FileInfo); 23 | -------------------------------------------------------------------------------- /src/meridian/testing/helpers.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import { ModalsProvider } from '@mantine/modals'; 5 | 6 | import { createStore } from 'meridian/state'; 7 | 8 | /* eslint-disable-next-line no-restricted-imports */ 9 | import { GlobalState } from 'meridian/state/types'; 10 | 11 | export const withReduxState = (element: React.ReactNode, state?: GlobalState) => ( 12 | {element} 13 | ); 14 | 15 | export const withMantineModals = (element: React.ReactNode) => ( 16 | {element} 17 | ); 18 | 19 | export const addTestId = (testId: string) => ({ 20 | ['data-testid']: testId, 21 | }); 22 | -------------------------------------------------------------------------------- /src/meridian/generic/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Page } from './page'; 2 | export { default as DrawerPage } from './drawerPage'; 3 | export { default as Dropzone } from './dropzone'; 4 | export { default as ContextMenu } from './contextMenu'; 5 | export { ColorSchemeToggle, SegmentedColorSchemeToggle } from './colorSchemeToggle'; 6 | export { default as useWindowSize } from './useWindowSize'; 7 | export { default as useIsSmallDevice } from './useIsSmallDevice'; 8 | export { default as ScrollToTopAffix } from './scrollToTopAffix'; 9 | export { default as LabelWithText } from './labelWithText'; 10 | export { default as LabelWithBadge } from './labelWithBadge'; 11 | export * from './commonConfigs'; 12 | export type { ContextMenuItem } from './contextMenu'; 13 | -------------------------------------------------------------------------------- /src/meridian/hooks/useToggleTheme.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { createSetSettingsAction, selectSettings } from 'meridian/settings'; 5 | 6 | export const useToggleTheme = () => { 7 | const settings = useSelector(selectSettings); 8 | const dispatch = useDispatch(); 9 | 10 | const theme = settings.darkMode ? 'dark' : 'light'; 11 | const toggleTheme = useCallback(() => { 12 | dispatch( 13 | createSetSettingsAction({ 14 | ...settings, 15 | darkMode: !settings.darkMode, 16 | }), 17 | ); 18 | }, [dispatch, settings]); 19 | 20 | return { 21 | toggleTheme, 22 | theme, 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/meridian/mock/mainData.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { MainData, TorrentInfo } from 'meridian/models'; 3 | 4 | import { MockCategories } from './categories'; 5 | import { MockTags } from './tags'; 6 | import { MockTorrents } from './torrents'; 7 | import { MockTransferInfo } from './transferInfo'; 8 | 9 | export const MockMainData: MainData = { 10 | categories: MockCategories, 11 | categories_removed: [], 12 | full_update: false, 13 | rid: 1, 14 | server_state: MockTransferInfo, 15 | tags: MockTags, 16 | tags_removed: [], 17 | torrents: (MockTorrents as TorrentInfo[]).reduce( 18 | (res, el) => ((res[el.hash] = el), res), 19 | {} as Record, 20 | ), 21 | torrents_removed: [], 22 | trackers: {}, 23 | }; 24 | -------------------------------------------------------------------------------- /src/meridian/models/torrentTracker.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/macro'; 2 | 3 | export interface TorrentTracker { 4 | url: string; 5 | status: TorrentTrackerStatus; 6 | tier: number; 7 | num_peers: number; 8 | num_seeds: number; 9 | num_leeches: number; 10 | num_downloaded: number; 11 | msg: string; 12 | } 13 | 14 | export enum TorrentTrackerStatus { 15 | DISABLED = 0, 16 | NOT_CONTACTED = 1, 17 | CONTACTED = 2, 18 | UPDATING = 3, 19 | ERROR = 4, 20 | } 21 | 22 | export const TorrentTrackerStatusDescription = { 23 | [TorrentTrackerStatus.DISABLED]: t`Disabled`, 24 | [TorrentTrackerStatus.NOT_CONTACTED]: t`Not contacted`, 25 | [TorrentTrackerStatus.CONTACTED]: t`Contacted`, 26 | [TorrentTrackerStatus.UPDATING]: t`Updating`, 27 | [TorrentTrackerStatus.ERROR]: t`Error`, 28 | }; 29 | -------------------------------------------------------------------------------- /src/meridian/models/sync.ts: -------------------------------------------------------------------------------- 1 | import { Category } from './category'; 2 | import { Peer } from './peer'; 3 | import { TorrentInfo } from './torrent'; 4 | import { TransferInfo } from './transferInfo'; 5 | 6 | export interface SyncMainData { 7 | rid: number; 8 | full_update?: boolean; 9 | categories?: { 10 | [name: string]: Category; 11 | }; 12 | categories_removed?: string[]; 13 | server_state?: TransferInfo; 14 | tags?: string[]; 15 | tags_removed?: string[]; 16 | torrents?: { 17 | [hash: string]: TorrentInfo; 18 | }; 19 | torrents_removed?: string[]; 20 | trackers?: { 21 | [url: string]: string[]; 22 | }; 23 | trackers_removed: string[]; 24 | } 25 | 26 | export interface SyncPeers { 27 | rid: number; 28 | peers: { 29 | [ip_and_port: string]: Peer; 30 | }; 31 | peers_removed?: string[]; 32 | } 33 | -------------------------------------------------------------------------------- /src/meridian/mock/preferences.ts: -------------------------------------------------------------------------------- 1 | import { Preferences } from 'meridian/models'; 2 | 3 | export const MockPreferences: Preferences = { 4 | create_subfolder_enabled: true, 5 | start_paused_enabled: false, 6 | preallocate_all: false, 7 | incomplete_files_ext: false, 8 | save_path: 'some/save/path', 9 | auto_tmm_enabled: true, 10 | category_changed_tmm_enabled: true, 11 | save_path_changed_tmm_enabled: true, 12 | torrent_changed_tmm_enabled: true, 13 | temp_path_enabled: true, 14 | temp_path: 'some/temp/path', 15 | autorun_enabled: true, 16 | autorun_program: 'some/autorun/program', 17 | alternative_webui_enabled: true, 18 | alternative_webui_path: 'some/webui/path', 19 | bypass_auth_subnet_whitelist_enabled: true, 20 | bypass_auth_subnet_whitelist: '1.1.1.1', 21 | web_ui_username: 'username', 22 | web_ui_password: 'password', 23 | }; 24 | -------------------------------------------------------------------------------- /src/meridian/preferences/modals/sections/speed/globalRateLimitsSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { t } from '@lingui/macro'; 4 | 5 | import { Accordion, NumberInput } from '@mantine/core'; 6 | 7 | import { SectionProps } from '../types'; 8 | 9 | const GlobalRateLimitsSection = ({ preferences, updatePreferencesKey }: SectionProps) => ( 10 | 11 | updatePreferencesKey('up_limit', value as number)} 15 | /> 16 | updatePreferencesKey('dl_limit', value as number)} 21 | /> 22 | 23 | ); 24 | 25 | export default GlobalRateLimitsSection; 26 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | 4 | import { ModalsProvider } from '@mantine/modals'; 5 | import { NotificationsProvider } from '@mantine/notifications'; 6 | 7 | import AppThemeProvider from 'meridian/ThemeProvider'; 8 | import { I18nProvider } from 'meridian/i18n'; 9 | import { AppRouter } from 'meridian/navigation'; 10 | import { createStore } from 'meridian/state'; 11 | 12 | const App = () => ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /src/meridian/mock/transferInfo.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionStatus, TransferInfo } from 'meridian/models'; 2 | 3 | export const MockTransferInfo: TransferInfo = { 4 | connection_status: ConnectionStatus.CONNECTED, 5 | dl_info_speed: 10000, 6 | dl_info_data: 200000, 7 | up_info_speed: 10000, 8 | up_info_data: 200000, 9 | dl_rate_limit: 4000, 10 | up_rate_limit: 4000, 11 | dht_nodes: 10, 12 | queueing: false, 13 | use_alt_speed_limits: false, 14 | refresh_interval: 5, 15 | alltime_dl: 10000, 16 | alltime_ul: 10000, 17 | average_time_queue: 10, 18 | free_space_on_disk: 10000, 19 | global_ratio: '10', 20 | queued_io_jobs: 0, 21 | read_cache_hits: '10', 22 | read_cache_overload: '10', 23 | total_buffers_size: 10000, 24 | total_peer_connections: 2, 25 | total_queued_size: 10, 26 | total_wasted_session: 10, 27 | write_cache_overload: '10', 28 | }; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": [ 6 | "DOM", 7 | "DOM.Iterable", 8 | "ESNext" 9 | ], 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx", 22 | "baseUrl": "src", 23 | "paths": { 24 | "meridian/*": [ 25 | "meridian/*" 26 | ], 27 | "locales/*": [ 28 | "locales/*" 29 | ], 30 | }, 31 | }, 32 | "include": [ 33 | "src" 34 | ], 35 | "references": [ 36 | { 37 | "path": "./tsconfig.node.json" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /src/meridian/generic/contextMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { MantineStyleSystemProps, Menu } from '@mantine/core'; 4 | 5 | export interface ContextMenuItem { 6 | text: string; 7 | icon?: React.ReactElement; 8 | callback: () => void; 9 | } 10 | 11 | interface Props extends MantineStyleSystemProps { 12 | items: ContextMenuItem[]; 13 | control: React.ReactElement; 14 | } 15 | 16 | const ContextMenu = ({ items, control, ...props }: Props) => ( 17 | 18 | {control} 19 | 20 | {items.map((item) => ( 21 | 22 | {item.text} 23 | 24 | ))} 25 | 26 | 27 | ); 28 | 29 | export default ContextMenu; 30 | -------------------------------------------------------------------------------- /src/meridian/home/hooks/useFetchTimer.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | import { useRefreshMainData } from 'meridian/mainData'; 5 | import { selectSettings } from 'meridian/settings'; 6 | 7 | const useFetchTimer = () => { 8 | const refreshMainData = useRefreshMainData(); 9 | const settings = useSelector(selectSettings); 10 | 11 | useEffect(() => { 12 | let timer: NodeJS.Timer; 13 | refreshMainData(); 14 | if (settings.autoRefresh) { 15 | timer = setInterval(() => { 16 | refreshMainData(); 17 | }, settings.autoRefreshInterval * 1000); 18 | } 19 | 20 | return () => { 21 | if (timer) { 22 | clearInterval(timer); 23 | } 24 | }; 25 | }, [refreshMainData, settings.autoRefresh, settings.autoRefreshInterval]); 26 | }; 27 | 28 | export default useFetchTimer; 29 | -------------------------------------------------------------------------------- /src/meridian/home/hooks/useManageSelection.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | 3 | const useManageSelection = () => { 4 | const [selectionEnabled, setSelectionEnabled] = useState(false); 5 | const [keys, setKeys] = useState([]); 6 | 7 | const onSelectionChanged = useCallback( 8 | (key: string, selected: boolean) => { 9 | if (!selected && keys.includes(key)) { 10 | setKeys(keys.filter((x) => x !== key)); 11 | } else if (selected && !keys.includes(key)) { 12 | setKeys([...keys, key]); 13 | } 14 | }, 15 | [keys, setKeys], 16 | ); 17 | 18 | const clearSelection = () => setKeys([]); 19 | 20 | return { 21 | selectionEnabled, 22 | setSelectionEnabled, 23 | keys, 24 | onSelectionChanged, 25 | clearSelection, 26 | }; 27 | }; 28 | 29 | export default useManageSelection; 30 | -------------------------------------------------------------------------------- /src/meridian/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | import { MantineProvider, MantineThemeOverride } from '@mantine/core'; 5 | 6 | import { selectSettings } from 'meridian/settings'; 7 | 8 | interface Props { 9 | children: React.ReactNode; 10 | } 11 | 12 | const ThemeProvider = ({ children }: Props) => { 13 | const settings = useSelector(selectSettings); 14 | const theme: MantineThemeOverride = useMemo( 15 | () => ({ 16 | colorScheme: settings.darkMode ? 'dark' : 'light', 17 | fontFamily: 'Inter var', 18 | fontSizes: { 19 | sm: 16, 20 | }, 21 | }), 22 | [settings], 23 | ); 24 | document.body.style.backgroundColor = settings.darkMode ? '#2C2E33' : '#FFFFFF'; 25 | 26 | return {children}; 27 | }; 28 | 29 | export default ThemeProvider; 30 | -------------------------------------------------------------------------------- /src/meridian/torrent/card/__tests__/card.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { render } from '@testing-library/react'; 4 | 5 | import { MockTorrents } from 'meridian/mock'; 6 | import { TorrentInfo } from 'meridian/models'; 7 | import { withMantineModals, withReduxState } from 'meridian/testing'; 8 | 9 | import TorrentCard from '../card'; 10 | 11 | describe('Card', () => { 12 | it('should render as expected', () => { 13 | const result = render( 14 | withReduxState( 15 | withMantineModals( 16 | , 22 | ), 23 | ), 24 | ); 25 | expect(result.asFragment()).toMatchSnapshot(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/meridian/home/headerContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Plus, Settings } from 'tabler-icons-react'; 4 | 5 | import { ActionIcon } from '@mantine/core'; 6 | 7 | import { ContextMenu } from 'meridian/generic'; 8 | import { useAddTorrentsModal } from 'meridian/torrent'; 9 | 10 | import { useHeaderMenuItems } from './hooks'; 11 | 12 | const HeaderContent = () => { 13 | const openAddTorrentsModal = useAddTorrentsModal(); 14 | const items = useHeaderMenuItems(); 15 | return ( 16 | <> 17 | 18 | 19 | 20 | 24 | 25 | 26 | } 27 | /> 28 | 29 | ); 30 | }; 31 | 32 | export default HeaderContent; 33 | -------------------------------------------------------------------------------- /src/meridian/preferences/modals/sections/connection/listeningPortSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { t } from '@lingui/macro'; 4 | 5 | import { Accordion, NumberInput, Switch } from '@mantine/core'; 6 | 7 | import { SectionProps } from '../types'; 8 | 9 | const ListeningPortSection = ({ preferences, updatePreferencesKey }: SectionProps) => ( 10 | 11 | updatePreferencesKey('listen_port', value as number)} 15 | /> 16 | updatePreferencesKey('upnp', value.target.checked)} 21 | /> 22 | 23 | ); 24 | 25 | export default ListeningPortSection; 26 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@5.0.2 5 | 6 | jobs: 7 | lint: 8 | executor: node/default 9 | steps: 10 | - checkout 11 | - node/install-packages: 12 | pkg-manager: yarn 13 | - run: 14 | command: yarn lint 15 | name: Check linting rules 16 | - run: 17 | command: yarn formatting 18 | name: Check formatting rules 19 | - run: 20 | command: yarn types 21 | name: Check types 22 | test: 23 | executor: node/default 24 | steps: 25 | - checkout 26 | - node/install-packages: 27 | pkg-manager: yarn 28 | - run: 29 | command: yarn test 30 | name: Run tests 31 | 32 | workflows: 33 | lint_and_test: 34 | jobs: 35 | - lint 36 | - test 37 | -------------------------------------------------------------------------------- /src/meridian/settings/modals/useSettings.ts: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | 4 | import { DefaultMantineColor } from '@mantine/core'; 5 | 6 | import { Settings, TorrentStateDescription } from 'meridian/models'; 7 | 8 | import { createSetSettingsAction, selectSettings } from '../state'; 9 | 10 | const useSettings = () => { 11 | const settings = useSelector(selectSettings); 12 | const dispatch = useDispatch(); 13 | 14 | const handleSettingsChange = useCallback( 15 | ( 16 | key: keyof Settings, 17 | value: boolean | number | string | Record, 18 | ) => { 19 | dispatch(createSetSettingsAction({ ...settings, [key]: value })); 20 | }, 21 | [dispatch, settings], 22 | ); 23 | 24 | return { 25 | settings, 26 | handleSettingsChange, 27 | }; 28 | }; 29 | 30 | export default useSettings; 31 | -------------------------------------------------------------------------------- /src/meridian/models/torrentProperties.ts: -------------------------------------------------------------------------------- 1 | export interface TorrentProperties { 2 | save_path: string; 3 | creation_date: number; 4 | piece_size: number; 5 | comment: string; 6 | total_wasted: number; 7 | total_uploaded: number; 8 | total_uploaded_session: number; 9 | total_downloaded: number; 10 | total_downloaded_session: number; 11 | up_limit: number; 12 | dl_limit: number; 13 | time_elapsed: number; 14 | seeding_time: number; 15 | nb_connections: number; 16 | nb_connections_limit: number; 17 | share_ratio: number; 18 | addition_date: number; 19 | created_by: string; 20 | dl_speed_avg: number; 21 | dl_speed: number; 22 | eta: number; 23 | last_seen: number; 24 | peers: number; 25 | peers_total: number; 26 | pieces_have: number; 27 | pieces_num: number; 28 | reannounce: number; 29 | seeds: number; 30 | seeds_total: number; 31 | total_size: number; 32 | up_speed_avg: number; 33 | up_speed: number; 34 | } 35 | -------------------------------------------------------------------------------- /src/meridian/navigation/router.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-imports */ 2 | import React, { useLayoutEffect, useState } from 'react'; 3 | import { Route, Router, Routes } from 'react-router-dom'; 4 | 5 | import HomePage from 'meridian/home/homePage'; 6 | import LoginPage from 'meridian/login/loginPage'; 7 | 8 | import { history } from './history'; 9 | import { AppRoutes } from './types'; 10 | 11 | const AppRouter = () => { 12 | const [state, setState] = useState({ 13 | action: history.action, 14 | location: history.location, 15 | }); 16 | 17 | useLayoutEffect(() => history.listen(setState), []); 18 | 19 | return ( 20 | 21 | 22 | } /> 23 | } /> 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default AppRouter; 30 | -------------------------------------------------------------------------------- /src/meridian/models/transferInfo.ts: -------------------------------------------------------------------------------- 1 | export enum ConnectionStatus { 2 | CONNECTED = 'connected', 3 | FIREWALED = 'firewaled', 4 | DISCONNECTED = 'disconnected', 5 | } 6 | 7 | export interface TransferInfo { 8 | alltime_dl: number; 9 | alltime_ul: number; 10 | average_time_queue: number; 11 | connection_status: ConnectionStatus; 12 | dht_nodes: number; 13 | dl_info_data: number; 14 | dl_info_speed: number; 15 | dl_rate_limit: number; 16 | free_space_on_disk: number; 17 | global_ratio: string; 18 | queued_io_jobs: number; 19 | queueing: boolean; 20 | read_cache_hits: string; 21 | read_cache_overload: string; 22 | refresh_interval: number; 23 | total_buffers_size: number; 24 | total_peer_connections: number; 25 | total_queued_size: number; 26 | total_wasted_session: number; 27 | up_info_data: number; 28 | up_info_speed: number; 29 | up_rate_limit: number; 30 | use_alt_speed_limits: boolean; 31 | write_cache_overload: string; 32 | } 33 | -------------------------------------------------------------------------------- /src/meridian/generic/__tests__/scrollToTopAffix.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { render, screen } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | 6 | const mockScrollTo = jest.fn(); 7 | 8 | jest.mock('@mantine/hooks', () => ({ 9 | ...jest.requireActual('@mantine/hooks'), 10 | useWindowScroll: jest.fn().mockReturnValue([{ y: 10 }, mockScrollTo]), 11 | })); 12 | 13 | import ScrollToTopAffix from '../scrollToTopAffix'; 14 | 15 | describe('ScrollToTopAffix', () => { 16 | beforeEach(() => { 17 | mockScrollTo.mockReset(); 18 | }); 19 | 20 | it('should render as expected', () => { 21 | const result = render(); 22 | expect(result.asFragment()).toMatchSnapshot(); 23 | }); 24 | 25 | it('should call scroll to top on click', async () => { 26 | render(); 27 | await userEvent.click(screen.getByTestId('affixButton')); 28 | expect(mockScrollTo).toHaveBeenCalled(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/meridian/generic/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Box, createStyles, useMantineTheme } from '@mantine/core'; 4 | 5 | interface Props { 6 | children: React.ReactNode; 7 | } 8 | 9 | const Page = ({ children }: Props) => { 10 | const styles = useStyles(); 11 | return ( 12 |
13 | {children} 14 |
15 | ); 16 | }; 17 | 18 | const useStyles = () => { 19 | const theme = useMantineTheme(); 20 | 21 | return createStyles({ 22 | root: { 23 | height: '100vh', 24 | display: 'flex', 25 | flexDirection: 'column', 26 | }, 27 | content: { 28 | display: 'flex', 29 | overflow: 'scroll', 30 | flex: 1, 31 | flexDirection: 'column', 32 | backgroundColor: theme.colorScheme === 'light' ? theme.white : theme.colors.dark[5], 33 | }, 34 | })(); 35 | }; 36 | 37 | export default Page; 38 | -------------------------------------------------------------------------------- /src/meridian/state/types.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-imports */ 2 | import { Resource, ResourceState } from 'meridian/resource'; 3 | import { SessionState } from 'meridian/session/state/types'; 4 | import { SettingsState } from 'meridian/settings/state/types'; 5 | import { TorrentFiltersState } from 'meridian/torrentFilters/state/types'; 6 | 7 | export interface GlobalState { 8 | mainDataState: ResourceState; 9 | torrentState: ResourceState; 10 | torrentPropertiesState: ResourceState; 11 | torrentContentState: ResourceState; 12 | torrentTrackersState: ResourceState; 13 | torrentFiltersState: TorrentFiltersState; 14 | sessionState: SessionState; 15 | settingsState: SettingsState; 16 | transferInfoState: ResourceState; 17 | preferencesState: ResourceState; 18 | categoriesState: ResourceState; 19 | tagsState: ResourceState; 20 | } 21 | -------------------------------------------------------------------------------- /src/meridian/preferences/modals/sections/downloads/addingTorrentSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { t } from '@lingui/macro'; 4 | 5 | import { Accordion, Switch } from '@mantine/core'; 6 | 7 | import { SectionProps } from '../types'; 8 | 9 | const AddingTorrentSection = ({ preferences, updatePreferencesKey }: SectionProps) => ( 10 | 11 | updatePreferencesKey('start_paused_enabled', value.target.checked)} 15 | /> 16 | 21 | updatePreferencesKey('create_subfolder_enabled', value.target.checked) 22 | } 23 | /> 24 | 25 | ); 26 | 27 | export default AddingTorrentSection; 28 | -------------------------------------------------------------------------------- /src/meridian/torrentFilters/state/reducer.ts: -------------------------------------------------------------------------------- 1 | import { LocalStorage, LocalStorageKey } from 'meridian/localStorage'; 2 | import { BaseAction } from 'meridian/resource'; 3 | 4 | import { SetTorrentFiltersAction, TorrentFiltersActions } from './actions'; 5 | import { TorrentFiltersState } from './types'; 6 | 7 | const initialState: TorrentFiltersState = { 8 | filters: { 9 | name: '', 10 | states: [], 11 | categories: [], 12 | tags: [], 13 | }, 14 | }; 15 | 16 | export const torrentFiltersReducer = ( 17 | state: TorrentFiltersState = initialState, 18 | action: BaseAction, 19 | ): TorrentFiltersState => { 20 | switch (action.type) { 21 | case TorrentFiltersActions.SET_TORRENT_FILTERS: { 22 | const setAction = action as SetTorrentFiltersAction; 23 | LocalStorage.setValue(LocalStorageKey.TORRENT_FILTERS, setAction.filters); 24 | return { 25 | ...state, 26 | filters: setAction.filters, 27 | }; 28 | } 29 | default: { 30 | return state; 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/meridian/generic/labelWithText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Box, 5 | DefaultMantineColor, 6 | MantineStyleSystemProps, 7 | Text, 8 | createStyles, 9 | } from '@mantine/core'; 10 | 11 | interface Props extends MantineStyleSystemProps { 12 | label: string; 13 | text: string; 14 | color?: DefaultMantineColor; 15 | icon?: React.ReactNode; 16 | } 17 | 18 | const LabelWithText = ({ label, text, color, icon, ...rest }: Props) => { 19 | const styles = useStyles(); 20 | 21 | return ( 22 | 23 | {icon} 24 | 25 | {label} 26 | 27 | {text} 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | const useStyles = createStyles({ 35 | root: { 36 | display: 'flex', 37 | flexDirection: 'row', 38 | alignItems: 'center', 39 | }, 40 | }); 41 | 42 | export default LabelWithText; 43 | -------------------------------------------------------------------------------- /src/meridian/torrent/card/connectionInfo.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | 3 | import { t } from '@lingui/macro'; 4 | 5 | import { Group, MantineStyleSystemProps } from '@mantine/core'; 6 | 7 | import { LabelWithText } from 'meridian/generic'; 8 | import { TORRENT_INVALID_ETA } from 'meridian/models'; 9 | import { calculateEtaString } from 'meridian/utils'; 10 | 11 | interface Props extends MantineStyleSystemProps { 12 | seeders: number; 13 | leechers: number; 14 | ratio: number; 15 | progress: number; 16 | eta: number; 17 | } 18 | 19 | const ConnectionInfo = ({ seeders, leechers, ratio, progress, eta, ...props }: Props) => ( 20 | 21 | 22 | 23 | 24 | {progress !== 1 && eta !== TORRENT_INVALID_ETA ? ( 25 | 26 | ) : null} 27 | 28 | ); 29 | 30 | export default memo(ConnectionInfo); 31 | -------------------------------------------------------------------------------- /src/meridian/i18n/i18nProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | 4 | import { i18n } from '@lingui/core'; 5 | import { I18nProvider as LibProvider } from '@lingui/react'; 6 | 7 | /* eslint-disable-next-line no-restricted-imports */ 8 | import { messages as enMessages } from 'locales/en/messages'; 9 | 10 | /* eslint-disable-next-line no-restricted-imports */ 11 | import { messages as roMessages } from 'locales/ro/messages'; 12 | 13 | import { selectSettings } from 'meridian/settings'; 14 | 15 | import { Language } from './types'; 16 | 17 | interface Props { 18 | children: React.ReactNode; 19 | } 20 | 21 | i18n.load({ 22 | [Language.ENGLISH]: enMessages, 23 | [Language.ROMANIAN]: roMessages, 24 | }); 25 | 26 | i18n.activate(Language.ENGLISH); 27 | 28 | const I18nProvider = ({ children }: Props) => { 29 | const settings = useSelector(selectSettings); 30 | 31 | useEffect(() => { 32 | i18n.activate(settings.language || Language.ENGLISH); 33 | }, [settings]); 34 | 35 | return {children}; 36 | }; 37 | 38 | export default I18nProvider; 39 | -------------------------------------------------------------------------------- /src/meridian/preferences/state/saga.ts: -------------------------------------------------------------------------------- 1 | import { t } from '@lingui/macro'; 2 | import { call, put, takeLatest } from 'redux-saga/effects'; 3 | 4 | import { 5 | Resource, 6 | ResourceSetAction, 7 | createFetchResourceSaga, 8 | createResourceFetchAction, 9 | createSetResourceSaga, 10 | } from 'meridian/resource'; 11 | import { showSnackbarAction } from 'meridian/snackbar'; 12 | 13 | import { PreferencesActions } from './reducer'; 14 | 15 | function* setPreferencesSaga(action: ResourceSetAction) { 16 | try { 17 | yield call(createSetResourceSaga(Resource.PREFERENCES), action); 18 | yield put(createResourceFetchAction(Resource.PREFERENCES)); 19 | yield put(showSnackbarAction(t`Preferences saved successfully!`, 'success', 2000)); 20 | } catch (error) { 21 | yield put(showSnackbarAction(t`Failed to save preferences!`, 'error', 2000)); 22 | } 23 | } 24 | 25 | function* preferencesSaga() { 26 | yield takeLatest(PreferencesActions.FETCH, createFetchResourceSaga(Resource.PREFERENCES)); 27 | yield takeLatest(PreferencesActions.POST, setPreferencesSaga); 28 | } 29 | 30 | export default preferencesSaga; 31 | -------------------------------------------------------------------------------- /src/meridian/preferences/modals/sections/connection/ipFilteringSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { t } from '@lingui/macro'; 4 | 5 | import { Accordion, Switch, TextInput, Textarea } from '@mantine/core'; 6 | 7 | import { SectionProps } from '../types'; 8 | 9 | const IpFilteringSection = ({ preferences, updatePreferencesKey }: SectionProps) => ( 10 | 11 | updatePreferencesKey('ip_filter_path', value.target.value)} 15 | /> 16 | updatePreferencesKey('ip_filter_trackers', value.target.checked)} 21 | /> 22 |