├── src ├── domain │ ├── __init__.py │ ├── progress.py │ ├── token_manager.py │ ├── clipboardproxy.py │ ├── config.py │ ├── threadpool.py │ └── backend.py ├── update_resources.bat ├── qml │ ├── themes │ │ ├── qmldir │ │ ├── DarkTheme.qml │ │ └── LightTheme.qml │ ├── components │ │ ├── convert │ │ │ ├── PasteConfirm.qml │ │ │ ├── CustomButton.qml │ │ │ ├── AudioTask.qml │ │ │ ├── CircularProgressBar.qml │ │ │ └── AcceptTasks.qml │ │ ├── custom │ │ │ └── CustomCheckBox.qml │ │ ├── settings │ │ │ ├── SettingsItem.qml │ │ │ └── SettingsDropDown.qml │ │ ├── SideBar.qml │ │ ├── ProcessPage.qml │ │ ├── ConvertPage.qml │ │ └── SettingsPage.qml │ ├── utils │ │ └── audiohelper.mjs │ ├── splash.qml │ ├── theme_demo.qml │ └── main.qml ├── resources │ ├── images │ │ ├── logo.icns │ │ ├── logo.ico │ │ ├── text_background_dark.png │ │ └── text_background_light.png │ ├── fonts │ │ └── Poppins.ttf │ ├── icons │ │ ├── arrow_left.svg │ │ ├── arrow_down.svg │ │ ├── close_circle.svg │ │ ├── audio.svg │ │ ├── link.svg │ │ ├── export.svg │ │ ├── ui_language.svg │ │ ├── part_max.svg │ │ ├── download.svg │ │ ├── checked.svg │ │ ├── select_model.svg │ │ ├── convert_language.svg │ │ ├── quit.svg │ │ ├── drop_empty.svg │ │ ├── convert_engine.svg │ │ ├── home_selected.svg │ │ ├── add_task.svg │ │ ├── video.svg │ │ ├── word_count.svg │ │ ├── folder.svg │ │ ├── home_unselected.svg │ │ ├── settings_unselected.svg │ │ ├── minimize.svg │ │ ├── settings_selected.svg │ │ ├── theme.svg │ │ └── key.svg │ └── languages.json ├── main.pyproject ├── main.py └── resources.qrc ├── poetry.toml ├── README.md ├── .gitignore ├── renovate.json5 ├── .github ├── ISSUE_TEMPLATE │ └── new_feature.md └── workflows │ └── release.yml ├── LICENSE ├── main.spec ├── pyproject.toml ├── .pre-commit-config.yaml └── almufarrigh.spec /src/domain/__init__.py: -------------------------------------------------------------------------------- 1 | """Domain package.""" 2 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | create = true 3 | in-project = true -------------------------------------------------------------------------------- /src/update_resources.bat: -------------------------------------------------------------------------------- 1 | pyside6-rcc resources.qrc -o resources_rc.py 2 | -------------------------------------------------------------------------------- /src/qml/themes/qmldir: -------------------------------------------------------------------------------- 1 | singleton LightTheme 1.0 LightTheme.qml 2 | singleton DarkTheme 1.0 DarkTheme.qml 3 | -------------------------------------------------------------------------------- /src/resources/images/logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ieasybooks/almufarrigh/HEAD/src/resources/images/logo.icns -------------------------------------------------------------------------------- /src/resources/images/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ieasybooks/almufarrigh/HEAD/src/resources/images/logo.ico -------------------------------------------------------------------------------- /src/resources/fonts/Poppins.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ieasybooks/almufarrigh/HEAD/src/resources/fonts/Poppins.ttf -------------------------------------------------------------------------------- /src/resources/images/text_background_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ieasybooks/almufarrigh/HEAD/src/resources/images/text_background_dark.png -------------------------------------------------------------------------------- /src/resources/images/text_background_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ieasybooks/almufarrigh/HEAD/src/resources/images/text_background_light.png -------------------------------------------------------------------------------- /src/resources/icons/arrow_left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/domain/progress.py: -------------------------------------------------------------------------------- 1 | """Progress domain model.""" 2 | 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class Progress: 8 | """Progress data class.""" 9 | 10 | value: float = 0.0 11 | remaining_time: float | None = None 12 | -------------------------------------------------------------------------------- /src/resources/icons/arrow_down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/resources/icons/close_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/resources/icons/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # المفرّغ 2 | 3 | [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/ieasybooks/almufarrigh/main.svg)](https://results.pre-commit.ci/latest/github/ieasybooks/almufarrigh/main) 4 | 5 | الواجهة الرسومية الخاصة بأداة تفريغ على أنظمة التشغيل المختلفة 6 | 7 | ## خطوات التشغيل 8 | 9 | نفذ الأمر التالي في داخل المجلد `src` لتوليد ملف الموارد 10 | ```bash 11 | cd src 12 | pyside6-rcc resources.qrc -o resources_rc.py 13 | ``` 14 | أو قم بتشغيل الملف الآتي: 15 | ```cmd 16 | cd src 17 | .\update_resources.bat 18 | ``` 19 | ثم قم بتشغل الملف الرئيسي: 20 | ```bash 21 | python main.py 22 | ``` 23 | -------------------------------------------------------------------------------- /src/resources/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/resources/icons/export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/qml/themes/DarkTheme.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 6.4 2 | pragma Singleton 3 | 4 | QtObject { 5 | property string theme_name: "غامق" 6 | property color primary: "#23A173" 7 | property color background: "#2C2C2C" 8 | property color card: "#363636" 9 | property color stroke: "#797979" 10 | property color field: "#575757" 11 | property color error: "#363636" 12 | property color fontPrimary: Qt.rgba(1, 1, 1, 0.87) 13 | property color fontSecondary: Qt.rgba(1, 1, 1, 0.6) 14 | property color fontThirty: Qt.rgba(1, 1, 1, 0.37) 15 | property var font: { 16 | name: 17 | "Poppins"; 18 | source: 19 | "qrc:/poppins"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/qml/themes/LightTheme.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 6.4 2 | pragma Singleton 3 | 4 | QtObject { 5 | property string theme_name: "فاتح" 6 | property color primary: "#4ED7A4" 7 | property color background: "#F1F1F1" 8 | property color card: "#FFFFFF" 9 | property color stroke: "#CCCCCC" 10 | property color field: "#F1F1F1" 11 | property color error: "#E06D6D" 12 | property color fontPrimary: Qt.rgba(0, 0, 0, 0.87) 13 | property color fontSecondary: Qt.rgba(0, 0, 0, 0.6) 14 | property color fontThirty: Qt.rgba(0, 0, 0, 0.37) 15 | property var font: { 16 | name: 17 | "Poppins"; 18 | source: 19 | "qrc:/poppins"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/resources/icons/ui_language.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/resources/icons/part_max.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.pyc 4 | *.pyo 5 | *.dll 6 | *.so 7 | *.pyd 8 | 9 | # Local build directories 10 | build/ 11 | dist/ 12 | 13 | # Ignore the compiled QML files 14 | *.qmlc 15 | 16 | # Ignore the cached QML files 17 | .qmlcache 18 | 19 | # Ignore user-specific settings 20 | .idea/ 21 | .vscode/ 22 | 23 | # Ignore virtual environment 24 | .env 25 | .venv 26 | env/ 27 | venv/ 28 | ENV/ 29 | env.bak/ 30 | venv.bak/ 31 | 32 | # macOS 33 | .DS_Store 34 | 35 | # tools versions managers 36 | .python-version 37 | .tool-versions 38 | .rtx.toml 39 | 40 | # Ruff 41 | .ruff_cache/ 42 | 43 | # mypy 44 | .dmypy.json 45 | .mypy_cache/ 46 | 47 | # Qt 48 | resources_rc.py 49 | main.pyproject.user 50 | 51 | # App settings 52 | settings.ini 53 | tokens.json 54 | -------------------------------------------------------------------------------- /src/resources/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | 4 | extends: [ 5 | // https://docs.renovatebot.com/presets-config/#configbase 6 | "config:base", 7 | // https://docs.renovatebot.com/presets-default/#enableprecommit 8 | ":enablePreCommit", 9 | // https://docs.renovatebot.com/presets-default/#rebasestaleprs 10 | ":rebaseStalePrs", 11 | ], 12 | 13 | // https://docs.renovatebot.com/configuration-options/#labels 14 | labels: ["dependencies"], 15 | 16 | // https://docs.renovatebot.com/configuration-options/#schedule 17 | schedule: ["before 5am on saturday"], 18 | 19 | // https://docs.renovatebot.com/configuration-options/#lockfilemaintenance 20 | lockFileMaintenance: { 21 | enabled: true, 22 | schedule: ["before 5am on saturday"], 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /src/resources/icons/checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/resources/icons/select_model.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/resources/icons/convert_language.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/resources/icons/quit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/domain/token_manager.py: -------------------------------------------------------------------------------- 1 | """Token manager module.""" 2 | 3 | import json 4 | from pathlib import Path 5 | from typing import cast 6 | 7 | 8 | class TokenManager: 9 | """Token manager class.""" 10 | 11 | def __init__(self, filename: str | None = None) -> None: 12 | """Initialize the token manager. 13 | 14 | :param filename: The filename to save the tokens to. 15 | """ 16 | self.filename: Path = Path(filename or "tokens.json") 17 | 18 | def read_tokens(self) -> dict[str, str]: 19 | """Read the tokens from the file.""" 20 | return cast( 21 | dict[str, str], 22 | json.loads(self.filename.read_text(encoding="utf-8")) if self.filename.exists() else {}, 23 | ) 24 | 25 | def save_tokens(self, tokens: dict[str, str]) -> None: 26 | """Save the tokens to the file.""" 27 | self.filename.write_text(json.dumps(tokens, ensure_ascii=False), encoding="utf-8") 28 | -------------------------------------------------------------------------------- /src/main.pyproject: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "main.py", 4 | "clipboardproxy.py", 5 | "controller.py", 6 | "resources.qrc", 7 | "update_resources.bat", 8 | "qml/components/convert/AcceptTasks.qml", 9 | "qml/components/convert/AudioTask.qml", 10 | "qml/components/convert/CustomButton.qml", 11 | "qml/components/convert/PasteConfirm.qml", 12 | "qml/components/ConvertPage.qml", 13 | "qml/components/settings/SettingsDropDown.qml", 14 | "qml/components/settings/SettingsItem.qml", 15 | "qml/components/custom/CustomCheckBox.qml", 16 | "qml/components/SettingsPage.qml", 17 | "qml/components/SideBar.qml", 18 | "qml/themes/DarkTheme.qml", 19 | "qml/themes/LightTheme.qml", 20 | "qml/themes/qmldir", 21 | "qml/main.qml", 22 | "qml/splash.qml", 23 | "qml/theme_demo.qml", 24 | "qml/utils/audiohelper.mjs" 25 | ] 26 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new_feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: فتح تذكرة 3 | about: إنشاء تذكرة جديدة ليعمل عليها الفريق 4 | title: '[Subject] <العنوان>' 5 | labels: 6 | assignees: '' 7 | 8 | --- 9 | 10 | # وصف التذكرة 11 | وصف واضح وكافي للتذكرة وما الغرض منها. 12 | 13 | # تصنيف التذكرة 14 | اختر تصنيفا وحيدا مناسبا للتذكرة من التبويب الجانبي. 15 | 16 | # الاختبار: 17 | اكتب كيفية اختبار هذه التذكرة بعد تنفيذها لنتأكد من عملها بهيئة سليمة. 18 | 19 | # المتطلبات السابقة: 20 | اكتب علي ماذا تعتمد هذه التذكرة. 21 | 22 | # المهام: 23 | في حالة كبر التذكرة قمسها لعدة مهام أصغر. 24 | يمكنك الإشارة إلى التذاكر بأرقامها أو روابطها، مثل هذا: 25 | - [x] #165 26 | 27 | 28 | # أفكار مستقبلية 29 | أضف أفكارًا مستقبلية أو أفكارًا بخصوص هذه الميزة. 30 | 31 | # ملاحظات إضافية 32 | أضف أي ملاحظات أو أفكار. 33 | 34 | # branches & pull request: 35 | عند العمل علي هذه التذكرة يجب أن تلتزم بقواعد التسمية كالأتي 36 | - branch: `label//` 37 | - pull request: `label--` 38 | -------------------------------------------------------------------------------- /src/resources/icons/drop_empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/resources/icons/convert_engine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/qml/components/convert/PasteConfirm.qml: -------------------------------------------------------------------------------- 1 | import QtQuick.Dialogs 2 | 3 | MessageDialog { 4 | id: pasteConfirmationDialog 5 | 6 | property url urlStr 7 | 8 | signal pasteConfirmed() 9 | 10 | function openWithUrl(_url) { 11 | urlStr = _url; 12 | text = qsTr("Do you want to paste the following YouTube URL?\n") + "\n" + _url; 13 | open(); 14 | } 15 | 16 | title: qsTr("Paste Confirmation") 17 | buttons: MessageDialog.Ok | MessageDialog.Cancel 18 | text: qsTr("Do you want to paste the text?") 19 | onAccepted: { 20 | // User clicked "Yes" button 21 | console.log("User clicked 'Yes'"); 22 | // Emit a signal to indicate the user wants to paste the text 23 | pasteConfirmed(); 24 | } 25 | onRejected: { 26 | // User clicked "No" button 27 | console.log("User clicked 'No'"); 28 | // Emit a signal to indicate the user doesn't want to paste the text 29 | pasteRejectedSignal(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/domain/clipboardproxy.py: -------------------------------------------------------------------------------- 1 | """Clipboard Warapper for sys clipboard so that it can be accessed from qml.""" 2 | 3 | from PySide6.QtCore import QObject, Signal, Slot 4 | 5 | 6 | class ClipboardProxy(QObject): 7 | """The proxy class. 8 | 9 | params: 10 | QObject (QObject): used by pyside 11 | """ 12 | 13 | def __init__(self, clipboard: QObject): 14 | """Initialize and connect signal for the clipboard. 15 | 16 | params: 17 | clipboard (QClipboard): System clipboard module 18 | """ 19 | super().__init__() 20 | self._clipboard = clipboard 21 | self._clipboard.dataChanged.connect(self.onClipboardDataChanged) 22 | 23 | @Slot(None, result=str) 24 | def getClipboardText(self) -> str: 25 | """Get the latest string from the clipboard. 26 | 27 | Returns 28 | ------- 29 | str: _description_ 30 | 31 | """ 32 | return str(self._clipboard.text()) 33 | 34 | textChanged = Signal() # noqa: N815 35 | 36 | def onClipboardDataChanged(self) -> None: 37 | self.textChanged.emit() 38 | -------------------------------------------------------------------------------- /src/qml/utils/audiohelper.mjs: -------------------------------------------------------------------------------- 1 | //TODO need further validation 2 | //what if it's a valid string but the video doesn't exist 3 | export function isYoutubeLink(text) { 4 | // Perform further actions with the clipboard text 5 | console.log("Clipboard text changed:", text) 6 | let youtubeUrlRegex = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/ 7 | if (!youtubeUrlRegex.test(text)) { 8 | return 9 | } 10 | return true 11 | } 12 | 13 | export function extractTextIdentifier(text) { 14 | if (isYoutubeLink(text)) 15 | return text 16 | else 17 | return text.substring(text.lastIndexOf("/") + 1) 18 | } 19 | 20 | export function getFileIcon(file) { 21 | var videoExtensions = ["mp4", "mov", "avi", "mkv"]; 22 | var audioExtensions = ["mp3", "wav", "ogg", "m4a"]; 23 | var extension = file.substring(file.lastIndexOf(".") + 1).toLowerCase() 24 | 25 | if (videoExtensions.includes(extension)) 26 | return "qrc:/video" 27 | else if (audioExtensions.includes(extension)) 28 | return "qrc:/audio" 29 | else 30 | return "qrc:/link" 31 | } 32 | -------------------------------------------------------------------------------- /src/resources/icons/home_selected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/qml/components/custom/CustomCheckBox.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 6.4 2 | import QtQuick.Controls 6.4 3 | import QtQuick.Controls.impl 4 | 5 | CheckBox { 6 | id: checkbox 7 | 8 | font.pixelSize: 16 9 | 10 | contentItem: Text { 11 | text: qsTr(checkbox.text) 12 | font: checkbox.font 13 | opacity: enabled ? 1 : 0.3 14 | color: theme.fontPrimary 15 | verticalAlignment: Text.AlignVCenter 16 | leftPadding: checkbox.indicator.width + checkbox.spacing 17 | } 18 | 19 | indicator: Rectangle { 20 | implicitWidth: 26 21 | implicitHeight: 26 22 | x: checkbox.leftPadding 23 | y: parent.height / 2 - height / 2 24 | radius: 8 25 | color: "transparent" 26 | border.color: checkbox.checked ? theme.primary : "#CACACA" 27 | border.width: 2 28 | 29 | IconImage { 30 | x: 6 31 | y: 6 32 | width: 14 33 | height: 14 34 | source: "qrc:/checked" 35 | color: theme.primary 36 | visible: checkbox.checked 37 | } 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 الكتب المُيسّرة 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/resources/icons/add_task.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/resources/icons/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/resources/icons/word_count.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/resources/icons/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/qml/components/convert/CustomButton.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 6.4 2 | import QtQuick.Controls 6.4 3 | 4 | Button { 5 | id: control 6 | 7 | property var backColor 8 | 9 | text: qsTr("Button") 10 | font.family: poppinsFontLoader.font.family 11 | font.pixelSize: 22 12 | font.weight: Font.Bold 13 | flat: true 14 | 15 | // Set hand cursor on hover 16 | MouseArea { 17 | id: mouseArea 18 | 19 | anchors.fill: parent 20 | hoverEnabled: true 21 | cursorShape: Qt.PointingHandCursor 22 | onClicked: control.clicked() 23 | } 24 | 25 | FontLoader { 26 | id: poppinsFontLoader 27 | 28 | source: theme.font.source 29 | } 30 | 31 | contentItem: Text { 32 | text: control.text 33 | font: control.font 34 | opacity: enabled ? 1 : 0.3 35 | // color: control.down ? "#17a81a" : "#21be2b" 36 | color: backColor ? theme.fontPrimary : Qt.rgba(255, 255, 255, 0.87) 37 | horizontalAlignment: Text.AlignHCenter 38 | verticalAlignment: Text.AlignVCenter 39 | elide: Text.ElideRight 40 | } 41 | 42 | background: Rectangle { 43 | implicitWidth: 100 44 | implicitHeight: 40 45 | color: backColor ? backColor : theme.primary 46 | opacity: enabled ? 1 : 0.3 47 | radius: 15 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /main.spec: -------------------------------------------------------------------------------- 1 | from os import name as os_name 2 | 3 | from PyInstaller.building.api import EXE, PYZ 4 | from PyInstaller.building.build_main import Analysis 5 | 6 | datas = [ 7 | ("src/qml", "qml/"), 8 | ("src/resources", "resources/"), 9 | ("src/resources_rc.py", "."), 10 | ] 11 | binaries = [] 12 | hidden_imports = [ 13 | "auditok", 14 | "numpy", 15 | "pydub", 16 | "requests", 17 | "scipy", 18 | "tqdm", 19 | "whisper", 20 | "faster-whisper", 21 | ] 22 | if os_name == "nt": 23 | binaries += [("ffmpeg.exe", "."), ("ffprobe.exe", ".")] 24 | 25 | a = Analysis( 26 | ["src/main.py"], 27 | pathex=[], 28 | binaries=binaries, 29 | datas=datas, 30 | hiddenimports=hidden_imports, 31 | hookspath=[], 32 | hooksconfig={}, 33 | runtime_hooks=[], 34 | excludes=[], 35 | noarchive=False, 36 | ) 37 | pyz = PYZ(a.pure) 38 | 39 | exe = EXE( 40 | pyz, 41 | a.scripts, 42 | a.binaries, 43 | a.datas, 44 | [], 45 | name="AlMufarrigh", 46 | icon="src/resources/images/icon.ico", 47 | debug=False, 48 | bootloader_ignore_signals=False, 49 | strip=False, 50 | upx=True, 51 | upx_exclude=[], 52 | runtime_tmpdir=None, 53 | console=False, 54 | disable_windowed_traceback=False, 55 | argv_emulation=False, 56 | target_arch=None, 57 | codesign_identity=None, 58 | entitlements_file=None, 59 | ) 60 | -------------------------------------------------------------------------------- /src/qml/components/convert/AudioTask.qml: -------------------------------------------------------------------------------- 1 | import "../../utils/audiohelper.mjs" as AudioHelper 2 | import QtQuick 6.4 3 | import QtQuick.Controls 6.4 4 | import QtQuick.Controls.impl 5 | import QtQuick.Layouts 6.4 6 | 7 | RowLayout { 8 | property string fileName 9 | 10 | signal removeAudioRequested() 11 | 12 | width: parent ? parent.width : 0 13 | Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter 14 | spacing: 5 15 | 16 | IconImage { 17 | width: 20 18 | height: 20 19 | source: AudioHelper.getFileIcon(fileName) 20 | color: theme.fontSecondary 21 | } 22 | 23 | Text { 24 | id: audioText 25 | 26 | property var maxLength: 50 27 | 28 | text: fileName.length > maxLength ? fileName.substring(0, maxLength) + '...' : fileName 29 | font.family: theme.font.name 30 | font.pixelSize: 20 31 | color: theme.fontSecondary 32 | horizontalAlignment: (Qt.locale().textDirection === Qt.RightToLeft) ? Text.AlignRight : Text.AlignLeft 33 | Layout.fillWidth: true 34 | LayoutMirroring.enabled: true 35 | } 36 | 37 | Rectangle { 38 | width: 32 39 | height: 24 40 | color: "transparent" 41 | 42 | IconImage { 43 | source: "qrc:/close_circle" 44 | } 45 | 46 | MouseArea { 47 | anchors.fill: parent 48 | hoverEnabled: true 49 | cursorShape: Qt.PointingHandCursor 50 | onClicked: removeAudioRequested() 51 | } 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | # ruff: noqa 2 | """Entry point for the application.""" 3 | 4 | import sys 5 | from pathlib import Path 6 | 7 | from PySide6.QtCore import QTimer, QUrl 8 | from PySide6.QtQml import QQmlApplicationEngine 9 | from PySide6.QtQuickControls2 import QQuickStyle 10 | from PySide6.QtWidgets import QApplication 11 | 12 | # noinspection PyUnresolvedReferences 13 | import resources_rc 14 | from domain.backend import Backend 15 | from domain.clipboardproxy import ClipboardProxy 16 | 17 | QQuickStyle.setStyle("Material") 18 | 19 | 20 | def load_main_window() -> None: 21 | engine.load(QUrl.fromLocalFile((path / "main.qml"))) 22 | if not engine.rootObjects(): 23 | sys.exit(-1) 24 | 25 | 26 | if __name__ == "__main__": 27 | app = QApplication(sys.argv) 28 | app.setOrganizationName("ieasybooks") 29 | app.setOrganizationDomain("https://almufaragh.com/") 30 | app.setApplicationName("Almufaragh") 31 | 32 | engine = QQmlApplicationEngine() 33 | backend = Backend() 34 | clipboard = QApplication.clipboard() 35 | clipboard_proxy = ClipboardProxy(clipboard) 36 | 37 | # Expose the Python object to QML 38 | engine.quit.connect(app.quit) 39 | engine.rootContext().setContextProperty("backend", backend) 40 | engine.rootContext().setContextProperty("clipboard", clipboard_proxy) 41 | path: Path = Path(__file__).parent / "qml" 42 | 43 | # Load splash screen 44 | engine.load(QUrl.fromLocalFile(path / "splash.qml")) 45 | if not engine.rootObjects(): 46 | sys.exit(-1) 47 | 48 | # Delay loading the main window 49 | QTimer.singleShot(1, load_main_window) # Adjust the delay time as needed 50 | 51 | app.exec() 52 | -------------------------------------------------------------------------------- /src/qml/components/settings/SettingsItem.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 6.4 2 | import QtQuick.Controls 6.4 3 | import QtQuick.Controls.impl 4 | import QtQuick.Layouts 6.4 5 | 6 | Rectangle { 7 | property string iconSource: "" 8 | property string labelText: "" 9 | default property alias children: myRow.children 10 | 11 | Layout.alignment: Qt.AlignVCenter 12 | color: theme.card 13 | Layout.fillWidth: true 14 | implicitHeight: 50 15 | radius: 8 16 | 17 | RowLayout { 18 | id: myRow 19 | 20 | Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter 21 | anchors.fill: parent 22 | anchors.leftMargin: 10 23 | anchors.rightMargin: 10 24 | spacing: 20 25 | layoutDirection: Qt.RightToLeft 26 | Layout.fillHeight: true 27 | 28 | FontLoader { 29 | id: poppinsFontLoader 30 | 31 | source: theme.font.source 32 | } 33 | 34 | IconImage { 35 | source: iconSource 36 | width: 20 37 | height: 20 38 | color: theme.fontPrimary 39 | Layout.fillHeight: true 40 | Layout.alignment: Qt.AlignVCenter 41 | } 42 | 43 | Text { 44 | text: labelText 45 | font.family: poppinsFontLoader.font.family 46 | font.pixelSize: 24 47 | color: theme.fontPrimary 48 | verticalAlignment: Text.AlignVCenter 49 | Layout.fillHeight: true 50 | Layout.alignment: Qt.AlignVCenter 51 | } 52 | 53 | Item { 54 | // Rectangle { anchors.fill: parent; color: "#ffaaaa" } // to visualize the spacer 55 | 56 | // spacer item 57 | Layout.fillWidth: true 58 | Layout.fillHeight: true 59 | } 60 | 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/domain/config.py: -------------------------------------------------------------------------------- 1 | """A module contains the configuration parser. 2 | 3 | It is used to read the settings.ini file and convert it to a pydantic model. 4 | """ 5 | 6 | import re 7 | from configparser import ConfigParser 8 | from pathlib import Path 9 | from typing import TypeVar, cast 10 | 11 | from pydantic import BaseModel 12 | 13 | T = TypeVar("T", bound=BaseModel) 14 | 15 | 16 | class AppConfig(BaseModel): 17 | """App configuration model.""" 18 | 19 | download_json: bool 20 | convert_engine: str 21 | save_location: str = str(Path.cwd()) 22 | word_count: int 23 | is_wit_engine: bool 24 | export_vtt: bool 25 | drop_empty_parts: bool 26 | max_part_length: float 27 | wit_convert_key: str 28 | whisper_model: str 29 | convert_language: str 30 | export_srt: bool 31 | export_txt: bool 32 | 33 | def get_output_formats(self) -> list[str]: 34 | formats = { 35 | "srt": self.export_srt, 36 | "vtt": self.export_vtt, 37 | "txt": self.export_txt, 38 | } 39 | return [key for key, value in formats.items() if value] 40 | 41 | 42 | class CaseSensitiveConfigParser(ConfigParser): 43 | """A case sensitive config parser.""" 44 | 45 | def optionxform(self, option_str: str) -> str: 46 | return option_str 47 | 48 | @staticmethod 49 | def camel_to_snake(camel_case: str) -> str: 50 | snake_case = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", camel_case) 51 | return snake_case.lower() 52 | 53 | @classmethod 54 | def read_config( 55 | cls, 56 | model: type[T] = AppConfig, 57 | filename: str = "settings.ini", 58 | default_section: str = "config", 59 | ) -> T: 60 | parser = cls(default_section=default_section) 61 | parser.read(filename) 62 | 63 | data = {cls.camel_to_snake(key): value for key, value in parser.defaults().items()} 64 | 65 | return cast(T, model(**data)) 66 | -------------------------------------------------------------------------------- /src/domain/threadpool.py: -------------------------------------------------------------------------------- 1 | """Custom thread class and signals emitted by worker threads.""" 2 | 3 | import sys 4 | import traceback 5 | from collections.abc import Callable, Generator 6 | from typing import Any 7 | 8 | from PySide6.QtCore import QObject, QRunnable, Signal 9 | 10 | from domain.progress import Progress 11 | 12 | 13 | class WorkerSignals(QObject): 14 | """Signals emitted by worker threads.""" 15 | 16 | finished = Signal() 17 | error = Signal(tuple) 18 | result = Signal(dict) 19 | progress = Signal(Progress) 20 | 21 | 22 | class Worker(QRunnable): 23 | """Custom thread class.""" 24 | 25 | def __init__( 26 | self, func: Callable[..., Generator[dict[str, int], None, None]], *args: Any, **kwargs: Any 27 | ) -> None: 28 | """Initialize worker object.""" 29 | super().__init__() 30 | self.func = func 31 | self.args = args 32 | self.kwargs = kwargs 33 | self.signals = WorkerSignals() 34 | 35 | def run(self) -> None: 36 | try: 37 | results = self.func(args=self.args, kwargs=self.kwargs) 38 | for result in results: 39 | progress_value, remaining_time = ( 40 | result["progress"], 41 | result["remaining_time"], 42 | ) 43 | progress = Progress( 44 | value=progress_value, 45 | remaining_time=remaining_time, 46 | ) 47 | self.signals.progress.emit(progress) 48 | self.signals.result.emit(result) 49 | except Exception: # noqa: BLE001 50 | traceback.print_exc() 51 | exc_type, value = sys.exc_info()[:2] 52 | self.signals.error.emit((exc_type, value, traceback.format_exc())) 53 | finally: 54 | try: 55 | self.signals.finished.emit() 56 | except RuntimeError: 57 | return 58 | 59 | def stop(self) -> None: 60 | self.terminate() 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "almufarrigh" 3 | version = "0.0.1" 4 | description = "الواجهة الرسومية الخاصة بأداة تفريغ على أنظمة التشغيل المختلفة" 5 | authors = ["ieasybooks "] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/ieasybooks/almufarrigh/" 9 | packages = [ 10 | { include = "src" }, 11 | ] 12 | 13 | [tool.poetry.dependencies] 14 | python = ">=3.12,<3.13" 15 | pyside6 = "^6.6.1" 16 | pydantic = "^2.5.2" 17 | 18 | [tool.poetry.group.wit] 19 | optional = true 20 | 21 | [tool.poetry.group.wit.dependencies] 22 | tafrigh = { version = "^1.6.2", extras = ["wit"] } 23 | 24 | [tool.poetry.group.whisper] 25 | optional = true 26 | 27 | [tool.poetry.group.whisper.dependencies] 28 | tafrigh = { version = "^1.6.2", extras = ["wit", "whisper"] } 29 | 30 | [tool.poetry.group.dev.dependencies] 31 | pre-commit = "^4.0.0" 32 | ruff = "^0.7.0" 33 | pyinstaller = "^6.3.0" 34 | pillow = "^11.0.0" 35 | 36 | [build-system] 37 | requires = ["poetry-core"] 38 | build-backend = "poetry.core.masonry.api" 39 | 40 | [tool.mypy] # https://mypy.readthedocs.io/en/latest/config_file.html 41 | files = ["src"] 42 | follow_imports = "skip" 43 | strict = true 44 | disallow_subclassing_any = false 45 | disallow_untyped_decorators = false 46 | ignore_missing_imports = true 47 | pretty = true 48 | show_column_numbers = true 49 | show_error_codes = true 50 | show_error_context = true 51 | warn_unreachable = true 52 | 53 | [tool.ruff] # https://github.com/charliermarsh/ruff 54 | fix = true 55 | line-length = 100 56 | src = ["src", "tests"] 57 | target-version = "py312" 58 | 59 | [tool.ruff.lint] 60 | select = ["A", "B", "BLE", "C4", "C90", "D", "DTZ", "E", "ERA", "F", "G", "I", "INP", "ISC", "N", "NPY", "PGH", "PIE", "PLC", "PLE", "PLR", "PLW", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "S", "SIM", "T10", "T20", "TID", "UP", "W", "YTT"] 61 | ignore = ["E501", "S307", "RET504", "S101", "D211", "D213", "N802", "D102", "ERA001", "ISC001", "D203"] 62 | unfixable = ["ERA001", "F401", "F841", "T201", "T203"] 63 | 64 | [tool.ruff.format] 65 | line-ending = "lf" 66 | quote-style = "double" 67 | indent-style = "space" 68 | -------------------------------------------------------------------------------- /src/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | resources/fonts/Poppins.ttf 4 | resources/icons/theme.svg 5 | resources/icons/add_task.svg 6 | resources/icons/export.svg 7 | resources/icons/key.svg 8 | resources/icons/download.svg 9 | resources/icons/folder.svg 10 | resources/icons/link.svg 11 | resources/icons/minimize.svg 12 | resources/icons/audio.svg 13 | resources/icons/quit.svg 14 | resources/icons/arrow_left.svg 15 | resources/icons/arrow_down.svg 16 | resources/icons/home_selected.svg 17 | resources/icons/home_unselected.svg 18 | resources/icons/settings_selected.svg 19 | resources/icons/settings_unselected.svg 20 | resources/icons/video.svg 21 | resources/icons/word_count.svg 22 | resources/icons/ui_language.svg 23 | resources/icons/convert_language.svg 24 | resources/images/logo.ico 25 | resources/images/text_background_light.png 26 | resources/images/text_background_dark.png 27 | resources/icons/select_model.svg 28 | resources/icons/part_max.svg 29 | resources/icons/drop_empty.svg 30 | resources/icons/convert_engine.svg 31 | resources/icons/close_circle.svg 32 | resources/icons/checked.svg 33 | 34 | -------------------------------------------------------------------------------- /src/resources/icons/home_unselected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/resources/icons/settings_unselected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/qml/components/convert/CircularProgressBar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 6.4 2 | import QtQuick.Controls 6.4 3 | 4 | Item { 5 | id: progressBar 6 | 7 | property int value: 0 8 | property int labelValue 9 | property int animationDuration: 1000 10 | property color foregroundColor: "#2196F3" 11 | property color backgroundColor: "#d1d1d1" 12 | 13 | width: 256 14 | height: 256 15 | onValueChanged: { 16 | canvas.degree = value; 17 | labelValue = value; 18 | } 19 | 20 | Canvas { 21 | id: canvas 22 | 23 | property int degree: 0 24 | 25 | antialiasing: true 26 | anchors.fill: parent 27 | onDegreeChanged: { 28 | requestPaint(); 29 | } 30 | onPaint: { 31 | const centerX = width / 2; 32 | const centerY = height / 2; 33 | const radius = Math.min(width, height) * 0.4; 34 | const startAngle = -90; // Start angle at the top (-90 degrees) 35 | const endAngle = startAngle + (degree / 100) * 360; // Calculate the end angle based on progress 36 | const ctx = getContext("2d"); 37 | ctx.clearRect(0, 0, width, height); // Clear the canvas 38 | // Draw the background circle 39 | ctx.strokeStyle = progressBar.backgroundColor; 40 | ctx.lineWidth = 20; 41 | ctx.beginPath(); 42 | ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); 43 | ctx.stroke(); 44 | // Draw the progress circle 45 | ctx.strokeStyle = progressBar.foregroundColor; 46 | ctx.beginPath(); 47 | ctx.arc(centerX, centerY, radius, startAngle * Math.PI / 180, endAngle * Math.PI / 180); 48 | ctx.stroke(); 49 | } 50 | 51 | Behavior on degree { 52 | NumberAnimation { 53 | duration: progressBar.animationDuration 54 | } 55 | 56 | } 57 | 58 | } 59 | 60 | Row { 61 | anchors.centerIn: parent 62 | 63 | Text { 64 | text: progressBar.labelValue 65 | font.pixelSize: 64 66 | color: theme.fontSecondary 67 | font.bold: true 68 | } 69 | 70 | Text { 71 | text: "%" 72 | font.pixelSize: 64 73 | color: theme.fontThirty 74 | font.bold: true 75 | } 76 | 77 | } 78 | 79 | Behavior on labelValue { 80 | NumberAnimation { 81 | duration: progressBar.animationDuration 82 | } 83 | 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/resources/icons/minimize.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/resources/icons/settings_selected.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # https://pre-commit.com 2 | #ci: 3 | # skip: [qmlformat, qmllint] 4 | default_install_hook_types: [ commit-msg, pre-commit ] 5 | default_stages: [ commit, manual ] 6 | fail_fast: true 7 | repos: 8 | - repo: meta 9 | hooks: 10 | - id: check-useless-excludes 11 | - repo: https://github.com/pre-commit/pygrep-hooks 12 | rev: v1.10.0 13 | hooks: 14 | - id: python-check-mock-methods 15 | - id: python-use-type-annotations 16 | - id: rst-backticks 17 | - id: rst-directive-colons 18 | - id: rst-inline-touching-normal 19 | - id: text-unicode-replacement-char 20 | - repo: https://github.com/pre-commit/pre-commit-hooks 21 | rev: v5.0.0 22 | hooks: 23 | - id: check-added-large-files 24 | - id: check-ast 25 | - id: check-builtin-literals 26 | - id: check-case-conflict 27 | - id: check-docstring-first 28 | - id: check-json 29 | - id: check-merge-conflict 30 | - id: check-shebang-scripts-are-executable 31 | - id: check-symlinks 32 | - id: check-toml 33 | - id: check-vcs-permalinks 34 | - id: check-xml 35 | - id: check-yaml 36 | - id: debug-statements 37 | - id: destroyed-symlinks 38 | - id: detect-private-key 39 | - id: end-of-file-fixer 40 | types: [ python ] 41 | - id: fix-byte-order-marker 42 | - id: mixed-line-ending 43 | - id: name-tests-test 44 | args: [ --pytest-test-first ] 45 | - id: trailing-whitespace 46 | types: [ python ] 47 | - repo: https://github.com/python-poetry/poetry 48 | rev: '1.8.4' # add version here 49 | hooks: 50 | - id: poetry-check 51 | # - id: poetry-lock 52 | - repo: https://github.com/MarcoGorelli/absolufy-imports 53 | rev: v0.3.1 54 | hooks: 55 | - id: absolufy-imports 56 | - repo: https://github.com/astral-sh/ruff-pre-commit 57 | # Ruff version. 58 | rev: 'v0.7.1' 59 | hooks: 60 | - id: ruff 61 | args: [ '--fix', '--exit-zero' ] 62 | - id: ruff-format 63 | - repo: https://github.com/pre-commit/mirrors-mypy 64 | rev: 'v1.13.0' # Use the sha / tag you want to point at 65 | hooks: 66 | - id: mypy 67 | # - repo: local 68 | # hooks: 69 | # - id: qmlformat 70 | # name: qmlformat 71 | # entry: qmlformat -i 72 | # pass_filenames: true 73 | # require_serial: true 74 | # language: system 75 | # types: [ text ] 76 | # files: ^.*\.qml$ 77 | # - id: qmllint 78 | # name: qmllint 79 | # entry: qmllint 80 | # pass_filenames: true 81 | # require_serial: true 82 | # language: system 83 | # types: [ text ] 84 | # files: ^.*\.qml$ -------------------------------------------------------------------------------- /src/resources/languages.json: -------------------------------------------------------------------------------- 1 | { 2 | "ar": "العــربية", 3 | "en": "الإنجليزية", 4 | "af": "الأفريكانية", 5 | "am": "الأمهرية", 6 | "as": "الأسامية", 7 | "az": "الأذربيجانية", 8 | "ba": "الباشكيرية", 9 | "be": "البيلاروسية", 10 | "bg": "البلغارية", 11 | "bn": "البنغالية", 12 | "bo": "التبتية", 13 | "br": "البريتونية", 14 | "bs": "البوسنية", 15 | "ca": "الكاتالانية", 16 | "cs": "التشيكية", 17 | "cy": "الولزية", 18 | "da": "الدانماركية", 19 | "de": "الألمانية", 20 | "el": "اليونانية", 21 | "es": "الإسبانية", 22 | "et": "الإستونية", 23 | "eu": "الباسكية", 24 | "fa": "الفارسية", 25 | "fi": "الفنلندية", 26 | "fo": "الفاروية", 27 | "fr": "الفرنسية", 28 | "gl": "الجاليكية", 29 | "gu": "الغوجاراتية", 30 | "ha": "الهوسا", 31 | "haw": "الهاوايية", 32 | "he": "العبرية", 33 | "hi": "الهندية", 34 | "hr": "الكرواتية", 35 | "ht": "الهايتية", 36 | "hu": "الهنغارية", 37 | "hy": "الأرمينية", 38 | "id": "الإندونيسية", 39 | "is": "الآيسلندية", 40 | "it": "الإيطالية", 41 | "ja": "اليابانية", 42 | "jw": "الجاوية", 43 | "ka": "الجورجية", 44 | "kk": "الكازاخية", 45 | "km": "الخميرية", 46 | "kn": "الكانادا", 47 | "ko": "الكورية", 48 | "la": "اللاتينية", 49 | "lb": "اللوكسمبورجية", 50 | "ln": "اللينغالية", 51 | "lo": "اللاو", 52 | "lt": "الليتوانية", 53 | "lv": "اللاتفية", 54 | "mg": "المدغشقرية", 55 | "mi": "الماورية", 56 | "mk": "المقدونية", 57 | "ml": "المالايالامية", 58 | "mn": "المنغولية", 59 | "mr": "الماراثية", 60 | "ms": "الماليزية", 61 | "mt": "المالطية", 62 | "my": "البورمية", 63 | "ne": "النيبالية", 64 | "nl": "الهولندية", 65 | "nn": "النينورتية", 66 | "no": "النرويجية", 67 | "oc": "الأوكيتانية", 68 | "pa": "البنجابية", 69 | "pl": "البولندية", 70 | "ps": "البشتونية", 71 | "pt": "البرتغالية", 72 | "ro": "الرومانية", 73 | "ru": "الروسية", 74 | "sa": "السنسكريتية", 75 | "sd": "السندية", 76 | "si": "السينهالية", 77 | "sk": "السلوفاكية", 78 | "sl": "السلوفينية", 79 | "sn": "الشونا", 80 | "so": "الصومالية", 81 | "sq": "الألبانية", 82 | "sr": "الصربية", 83 | "su": "السودانية", 84 | "sv": "السويدية", 85 | "sw": "السواحلية", 86 | "ta": "التاميلية", 87 | "te": "التيلجو", 88 | "tg": "الطاجيكية", 89 | "th": "التايلاندية", 90 | "tk": "التركمانية", 91 | "tl": "التاجالوجية", 92 | "tr": "التركية", 93 | "tt": "التتارية", 94 | "uk": "الأوكرانية", 95 | "ur": "الأردية", 96 | "uz": "الأوزبكية", 97 | "vi": "الفيتنامية", 98 | "yi": "اليديشية", 99 | "yo": "اليوروبا", 100 | "zh": "الصينية" 101 | } -------------------------------------------------------------------------------- /src/qml/splash.qml: -------------------------------------------------------------------------------- 1 | import QtCore 2 | import QtQuick 6.4 3 | import QtQuick.Controls 6.4 4 | import QtQuick.Controls.impl 5 | import QtQuick.Controls.Material 6.4 6 | import QtQuick.Layouts 6.4 7 | import "themes" 8 | 9 | ApplicationWindow { 10 | id: splash 11 | 12 | property bool isLightTheme: true 13 | property var theme: isLightTheme ? LightTheme : DarkTheme 14 | 15 | width: 600 16 | height: 400 17 | visible: true 18 | flags: Qt.SplashScreen 19 | 20 | Rectangle { 21 | anchors.fill: parent 22 | color: theme.background 23 | 24 | IconImage { 25 | id: logo 26 | 27 | source: "qrc:/logo" 28 | fillMode: Image.PreserveAspectFit 29 | height: 180 30 | width: 180 31 | 32 | anchors { 33 | top: parent.top 34 | right: parent.right 35 | left: parent.left 36 | topMargin: 64 37 | } 38 | 39 | } 40 | 41 | Text { 42 | id: appName 43 | 44 | color: theme.fontPrimary 45 | text: qsTr('الـــمــفرغ') 46 | font.pointSize: 24 47 | font.weight: Font.Medium 48 | horizontalAlignment: Text.AlignHCenter 49 | 50 | anchors { 51 | top: logo.bottom 52 | left: logo.left 53 | right: logo.right 54 | topMargin: 16 55 | } 56 | 57 | } 58 | 59 | Column { 60 | id: copyRights 61 | 62 | anchors { 63 | bottom: parent.bottom 64 | horizontalCenter: parent.horizontalCenter 65 | bottomMargin: 28 66 | } 67 | 68 | Text { 69 | text: "Copyright © 2022-2023 almufaragh.com." 70 | color: theme.fontThirty 71 | font.pointSize: 12 72 | anchors.horizontalCenter: parent.horizontalCenter 73 | } 74 | 75 | Text { 76 | text: qsTr("الإصدار") + " 1.0.6 " 77 | color: theme.fontThirty 78 | font.pointSize: 12 79 | anchors.horizontalCenter: parent.horizontalCenter 80 | } 81 | 82 | } 83 | 84 | } 85 | 86 | Timer { 87 | id: timer 88 | 89 | interval: 3500 90 | running: true 91 | repeat: false 92 | onTriggered: { 93 | timer.stop(); 94 | splash.close(); 95 | } 96 | } 97 | 98 | Settings { 99 | id: settings 100 | 101 | property alias isLightTheme: splash.isLightTheme 102 | 103 | category: "app" 104 | location: "file:settings.ini" 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/resources/icons/theme.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /almufarrigh.spec: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | from importlib.metadata import PackageNotFoundError 3 | from os import name as os_name 4 | from shutil import which 5 | 6 | from PyInstaller.building.api import COLLECT, EXE, PYZ 7 | from PyInstaller.building.build_main import Analysis 8 | from PyInstaller.building.osx import BUNDLE 9 | from PyInstaller.utils.hooks import collect_data_files, copy_metadata 10 | 11 | datas = [ 12 | ("src/qml", "qml/"), 13 | ("src/resources", "resources/"), 14 | ("src/resources_rc.py", "."), 15 | ] 16 | datas += collect_data_files("whisper") 17 | datas += collect_data_files("transformers", include_py_files=True) 18 | datas += collect_data_files("torch") 19 | with suppress(PackageNotFoundError): 20 | datas += copy_metadata("torch") 21 | datas += copy_metadata("tqdm") 22 | with suppress(PackageNotFoundError): 23 | datas += copy_metadata("numpy") 24 | datas += copy_metadata("requests") 25 | datas += copy_metadata("pydub") 26 | datas += copy_metadata("auditok") 27 | datas += copy_metadata("tafrigh") 28 | 29 | binaries = [] 30 | if os_name == "nt": 31 | binaries += [("ffmpeg.exe", "."), ("ffprobe.exe", ".")] 32 | else: 33 | if ffmpeg_path := which("ffmpeg"): 34 | datas += [(ffmpeg_path, ".")] 35 | 36 | a = Analysis( 37 | ["src/main.py"], 38 | pathex=[], 39 | binaries=binaries, 40 | datas=datas, 41 | hiddenimports=[], 42 | hookspath=[], 43 | hooksconfig={}, 44 | win_no_prefer_redirects=False, 45 | win_private_assemblies=False, 46 | cipher=None, 47 | runtime_hooks=[], 48 | excludes=[ 49 | "regex", 50 | "tkinter", 51 | "lib2to3", 52 | "unittest", 53 | "test", 54 | "websockets", 55 | "xmlrpc", 56 | "chardet", 57 | "pyreadline", 58 | "pycparser", 59 | "pydoc_data", 60 | ], 61 | noarchive=False, 62 | ) 63 | pyz = PYZ(a.pure, a.zipped_data, cipher=None) 64 | 65 | exe = EXE( 66 | pyz, 67 | a.scripts, 68 | [], 69 | name="AlMufarrigh", 70 | icon="./src/resources/images/logo.ico", 71 | exclude_binaries=True, 72 | debug=True, 73 | bootloader_ignore_signals=False, 74 | strip=False, 75 | upx=True, 76 | upx_exclude=[], 77 | runtime_tmpdir=None, 78 | console=False, 79 | disable_windowed_traceback=False, 80 | argv_emulation=False, 81 | target_arch=None, 82 | codesign_identity=None, 83 | entitlements_file=None, 84 | ) 85 | 86 | coll = COLLECT( 87 | exe, 88 | a.binaries, 89 | a.zipfiles, 90 | a.datas, 91 | strip=False, 92 | upx=False, 93 | upx_exclude=[], 94 | name="almufarrigh", 95 | ) 96 | app = BUNDLE( 97 | coll, 98 | name="AlMufarrigh.app", 99 | icon="./src/resources/images/logo.icns", 100 | bundle_identifier="com.almufarrigh", 101 | version="0.1.0", 102 | info_plist={ 103 | "NSPrincipalClass": "NSApplication", 104 | "NSHighResolutionCapable": "True", 105 | }, 106 | ) 107 | -------------------------------------------------------------------------------- /src/qml/theme_demo.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.6 2 | import QtQuick.Controls 2.12 3 | import QtQuick.Controls.Material 2.12 4 | import QtQuick.Layouts 1.12 5 | import QtQuick.Window 2.12 6 | import "Themes" 7 | 8 | ApplicationWindow { 9 | property bool isLightTheme: false 10 | property var theme: isLightTheme ? LightTheme : DarkTheme 11 | 12 | width: 400 13 | height: 800 14 | visible: true 15 | color: "#CCCCCC" 16 | LayoutMirroring.enabled: true 17 | 18 | Image { 19 | source: "resources/Logo.png" 20 | } 21 | 22 | Column { 23 | spacing: 10 24 | padding: 10 25 | 26 | FontLoader { 27 | id: poppinsFont 28 | 29 | source: "../resources/Fonts/Poppins-Regular.ttf" // Adjust the path to the correct location of the font file 30 | } 31 | 32 | Switch { 33 | id: themeSwitch 34 | 35 | text: "Dark / Light" 36 | onToggled: { 37 | isLightTheme = themeSwitch.checked; 38 | theme = isLightTheme ? LightTheme : DarkTheme; 39 | } 40 | } 41 | 42 | Text { 43 | text: theme.theme_name 44 | font.family: poppinsFont.name 45 | font.pointSize: 24 46 | } 47 | 48 | Rectangle { 49 | width: 100 50 | height: 30 51 | color: theme.primary 52 | } 53 | 54 | Text { 55 | text: "primary: " + theme.primary 56 | } 57 | 58 | Rectangle { 59 | width: 100 60 | height: 30 61 | color: theme.background 62 | } 63 | 64 | Text { 65 | text: "background: " + theme.background 66 | } 67 | 68 | Rectangle { 69 | width: 100 70 | height: 30 71 | color: theme.card 72 | } 73 | 74 | Text { 75 | text: "card: " + theme.card 76 | } 77 | 78 | Rectangle { 79 | width: 100 80 | height: 30 81 | color: theme.stroke 82 | } 83 | 84 | Text { 85 | text: "stroke: " + theme.stroke 86 | } 87 | 88 | Rectangle { 89 | width: 100 90 | height: 30 91 | color: theme.error 92 | } 93 | 94 | Text { 95 | text: "error: " + theme.error 96 | } 97 | 98 | Rectangle { 99 | width: 100 100 | height: 30 101 | color: theme.fontPrimary 102 | } 103 | 104 | Text { 105 | text: "fontPrimary: " + theme.fontPrimary 106 | } 107 | 108 | Rectangle { 109 | width: 100 110 | height: 30 111 | color: theme.fontSecondary 112 | } 113 | 114 | Text { 115 | text: "fontSecondary: " + theme.fontSecondary 116 | } 117 | 118 | Rectangle { 119 | width: 100 120 | height: 30 121 | color: theme.fontThirty 122 | } 123 | 124 | Text { 125 | text: "fontThirty: " + theme.fontThirty 126 | } 127 | 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/qml/components/SideBar.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 6.4 2 | import QtQuick.Controls 6.4 3 | import QtQuick.Controls.impl 4 | import QtQuick.Layouts 6.4 5 | 6 | Rectangle { 7 | property int index: 0 8 | 9 | signal sidebarButtonClicked(int index) 10 | signal folderClick() 11 | 12 | color: theme.primary 13 | width: 80 14 | height: parent.height 15 | 16 | IconImage { 17 | source: "qrc:/logo" 18 | width: 50 19 | height: 50 20 | 21 | anchors { 22 | top: parent.top 23 | left: parent.left 24 | right: parent.right 25 | topMargin: 16 26 | } 27 | 28 | } 29 | 30 | ColumnLayout { 31 | spacing: 20 32 | 33 | anchors { 34 | bottom: parent.bottom 35 | left: parent.left 36 | right: parent.right 37 | bottomMargin: 24 38 | } 39 | 40 | IconImage { 41 | source: index === 0 ? "qrc:/home_selected" : "qrc:/home_unselected" 42 | Layout.alignment: Qt.AlignHCenter 43 | 44 | MouseArea { 45 | anchors.fill: parent 46 | hoverEnabled: true 47 | cursorShape: Qt.PointingHandCursor 48 | onClicked: { 49 | index = 0; 50 | sidebarButtonClicked(0); 51 | } 52 | } 53 | 54 | } 55 | 56 | IconImage { 57 | source: index === 1 ? "qrc:/settings_selected" : "qrc:/settings_unselected" 58 | Layout.alignment: Qt.AlignHCenter 59 | 60 | MouseArea { 61 | anchors.fill: parent 62 | hoverEnabled: true 63 | cursorShape: Qt.PointingHandCursor 64 | onClicked: { 65 | index = 1; 66 | sidebarButtonClicked(1); 67 | } 68 | } 69 | 70 | } 71 | 72 | Rectangle { 73 | height: 1 74 | color: theme.background 75 | Layout.fillWidth: true 76 | } 77 | 78 | IconImage { 79 | source: "qrc:/folder" 80 | Layout.alignment: Qt.AlignHCenter 81 | 82 | MouseArea { 83 | anchors.fill: parent 84 | hoverEnabled: true 85 | cursorShape: Qt.PointingHandCursor 86 | onClicked: folderClick() 87 | } 88 | 89 | } 90 | 91 | IconImage { 92 | source: "qrc:/minimize" 93 | Layout.alignment: Qt.AlignHCenter 94 | 95 | MouseArea { 96 | anchors.fill: parent 97 | hoverEnabled: true 98 | cursorShape: Qt.PointingHandCursor 99 | onClicked: mainWindow.showMinimized() 100 | } 101 | 102 | } 103 | 104 | IconImage { 105 | source: "qrc:/quit" 106 | Layout.alignment: Qt.AlignHCenter 107 | 108 | MouseArea { 109 | anchors.fill: parent 110 | hoverEnabled: true 111 | cursorShape: Qt.PointingHandCursor 112 | onClicked: Qt.quit() 113 | } 114 | 115 | } 116 | 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/qml/components/settings/SettingsDropDown.qml: -------------------------------------------------------------------------------- 1 | import "." 2 | import QtQuick 6.4 3 | import QtQuick.Controls 6.4 4 | import QtQuick.Controls.impl 5 | 6 | SettingsItem { 7 | property alias selectedValue: combo.currentValue 8 | property alias currentIndex: combo.currentIndex 9 | property ListModel dropdownModel 10 | 11 | signal changedSelection(int index, string value) 12 | 13 | ComboBox { 14 | id: combo 15 | 16 | textRole: "text" 17 | valueRole: "value" 18 | font.family: poppinsFontLoader.font.family 19 | model: dropdownModel 20 | onActivated: (index) => { 21 | const selectedValue = model.get(index)[combo.valueRole]; 22 | changedSelection(index, selectedValue); 23 | combo.currentIndex = index; 24 | } 25 | 26 | FontLoader { 27 | id: poppinsFontLoader 28 | 29 | source: theme.font.source 30 | } 31 | 32 | background: Rectangle { 33 | implicitWidth: 100 34 | implicitHeight: 40 35 | radius: 8 36 | border.color: theme.stroke 37 | border.width: 2 38 | color: theme.field 39 | } 40 | 41 | delegate: ItemDelegate { 42 | width: combo.width 43 | height: 80 44 | highlighted: combo.highlightedIndex === index 45 | 46 | contentItem: Text { 47 | anchors.centerIn: parent 48 | text: model[combo.textRole] 49 | color: theme.fontPrimary 50 | font.family: theme.font.name 51 | font.pixelSize: 16 52 | elide: Text.ElideRight 53 | verticalAlignment: Text.AlignVCenter 54 | } 55 | 56 | } 57 | 58 | contentItem: Text { 59 | leftPadding: combo.indicator.width + combo.spacing + 24 60 | text: combo.displayText 61 | font.family: theme.font.name 62 | font.pixelSize: 22 63 | color: combo.pressed ? theme.fontSecondary : theme.fontPrimary 64 | verticalAlignment: Text.AlignVCenter 65 | elide: Text.ElideRight 66 | } 67 | 68 | indicator: IconImage { 69 | source: "qrc:/arrow_down" 70 | color: theme.fontPrimary 71 | anchors.left: parent.left 72 | y: (combo.background.implicitHeight / 2) - 4 73 | anchors.leftMargin: 16 74 | } 75 | 76 | popup: Popup { 77 | y: combo.height - 1 78 | width: combo.width 79 | implicitHeight: contentItem.implicitHeight 80 | padding: 1 81 | 82 | contentItem: ListView { 83 | clip: true 84 | implicitHeight: contentHeight 85 | model: combo.popup.visible ? combo.delegateModel : null 86 | currentIndex: combo.highlightedIndex 87 | 88 | ScrollIndicator.vertical: ScrollIndicator { 89 | } 90 | 91 | } 92 | 93 | background: Rectangle { 94 | color: theme.field 95 | radius: 8 96 | } 97 | 98 | } 99 | 100 | } 101 | 102 | dropdownModel: ListModel { 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/resources/icons/key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/qml/main.qml: -------------------------------------------------------------------------------- 1 | import QtCore 2 | import QtQuick 6.4 3 | import QtQuick.Controls 6.4 4 | import QtQuick.Controls.Material 6.4 5 | import QtQuick.Layouts 6.4 6 | import QtQuick.Window 6.4 7 | import "components" 8 | import "components/convert" 9 | import "themes" 10 | 11 | ApplicationWindow { 12 | id: mainWindow 13 | 14 | property bool isLightTheme: true 15 | property var theme: isLightTheme ? LightTheme : DarkTheme 16 | 17 | // REMOVE TITLE BAR 18 | flags: (Qt.FramelessWindowHint | Qt.Window) 19 | width: 1024 20 | height: 840 21 | visible: true 22 | Material.accent: theme.primary 23 | Material.primary: theme.primary 24 | 25 | // Dragging functionality 26 | MouseArea { 27 | id: dragArea 28 | 29 | property point clickPos 30 | 31 | anchors.fill: parent 32 | hoverEnabled: true 33 | onPressed: (mouse) => { 34 | clickPos = Qt.point(mouse.x, mouse.y); 35 | } 36 | onMouseXChanged: (mouse) => { 37 | if (mouse.buttons === Qt.LeftButton) 38 | mainWindow.x += (mouse.x - clickPos.x); 39 | 40 | } 41 | onMouseYChanged: (mouse) => { 42 | if (mouse.buttons === Qt.LeftButton) 43 | mainWindow.y += (mouse.y - clickPos.y); 44 | 45 | } 46 | } 47 | 48 | SideBar { 49 | id: sidebar 50 | 51 | anchors.top: parent.top 52 | anchors.bottom: parent.bottom 53 | anchors.left: parent.left 54 | onFolderClick: { 55 | backend.open_folder(settingsPage.saveLocation); 56 | } 57 | onSidebarButtonClicked: (index) => { 58 | stackLayout.currentIndex = index; 59 | } 60 | } 61 | 62 | StackLayout { 63 | id: stackLayout 64 | 65 | anchors { 66 | left: sidebar.right 67 | right: parent.right 68 | top: parent.top 69 | bottom: parent.bottom 70 | } 71 | 72 | ConvertPage { 73 | id: convertPage 74 | 75 | function clearAudioFiles() { 76 | audioFilesModel.clear(); 77 | } 78 | 79 | onConvertRequested: (urls) => { 80 | backend.urls = urls; 81 | backend.start(); 82 | stackLayout.currentIndex = 2; 83 | } 84 | // Connect to the signal and update completionTextDisplayed 85 | onStartConversionClicked: { 86 | processPage.completionTextDisplayed = false; 87 | } 88 | } 89 | 90 | SettingsPage { 91 | id: settingsPage 92 | 93 | onThemeChanged: (state) => { 94 | let isLightTheme = !state; 95 | } 96 | } 97 | 98 | ProcessPage { 99 | id: processPage 100 | } 101 | 102 | Connections { 103 | target: processPage 104 | onClearAudioFiles: { 105 | convertPage.clearAudioFiles(); 106 | } 107 | } 108 | 109 | } 110 | 111 | Settings { 112 | id: settings 113 | 114 | property alias isLightTheme: mainWindow.isLightTheme 115 | 116 | category: "app" 117 | location: "file:settings.ini" 118 | } 119 | 120 | Connections { 121 | function onFinish() { 122 | timer.start(); 123 | } 124 | 125 | target: backend 126 | enabled: mainWindow.visible 127 | } 128 | 129 | Timer { 130 | id: timer 131 | 132 | interval: 1000 133 | running: false 134 | repeat: false 135 | onTriggered: { 136 | timer.stop(); 137 | stackLayout.currentIndex = 0; 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Package Application 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version (semantic)" 8 | required: true 9 | default: "X.X" 10 | spec: 11 | description: "Spec file" 12 | required: true 13 | default: "almufarrigh.spec" 14 | 15 | jobs: 16 | build: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ 'windows-latest', 'macos-latest' ] 22 | python-version: [ "3.12" ] 23 | poetry-version: [ "1.8.4" ] 24 | variant: [ 'wit', 'whisper' ] 25 | architecture: [ 'x64' ] 26 | # include: 27 | # - os: windows-latest 28 | # architecture: 'x86' 29 | # python-version: "3.9" 30 | # poetry-version: "1.8.4" 31 | # variant: "wit" 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-python@v5 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | architecture: ${{ matrix.architecture }} 38 | - name: Caching 39 | uses: actions/cache@v4 40 | with: 41 | path: ./.venv 42 | key: venv-${{ runner.os }}-${{ hashFiles('poetry.lock') }} 43 | - name: Install requirements 44 | uses: abatilo/actions-poetry@v2 45 | with: 46 | poetry-version: ${{ matrix.poetry-version }} 47 | - run: poetry install --with ${{ matrix.variant }} --with dev 48 | - name: Download ffmpeg (Windows) 49 | if: runner.os == 'Windows' 50 | run: | 51 | Invoke-WebRequest https://github.com/GyanD/codexffmpeg/releases/download/6.0/ffmpeg-6.0-essentials_build.zip -OutFile ffmpeg.zip 52 | tar.exe -xf ffmpeg.zip 53 | move ffmpeg-6.0-essentials_build/bin/ffmpeg.exe ffmpeg.exe 54 | move ffmpeg-6.0-essentials_build/bin/ffprobe.exe ffprobe.exe 55 | - name: Download FFmpeg and FFprobe (Linux) 56 | if: runner.os == 'Linux' 57 | run: | 58 | wget https://www.johnvansickle.com/ffmpeg/old-releases/ffmpeg-6.0-amd64-static.tar.xz 59 | tar xvf ffmpeg-6.0-amd64-static.tar.xz 60 | mv ffmpeg-6.0-amd64-static/ffmpeg . 61 | mv ffmpeg-6.0-amd64-static/ffprobe . 62 | chmod +x ffmpeg ffprobe 63 | - name: Download FFmpeg and FFprobe (macOS) 64 | if: runner.os == 'macOS' 65 | run: | 66 | curl -L https://evermeet.cx/pub/ffmpeg/ffmpeg-6.0.zip -o ffmpeg.zip 67 | curl -L https://evermeet.cx/pub/ffprobe/ffprobe-6.0.zip -o ffprobe.zip 68 | unzip ffmpeg.zip 69 | unzip ffprobe.zip 70 | chmod +x ffmpeg ffprobe 71 | - name: Package 72 | run: | 73 | poetry run pyside6-rcc src/resources.qrc -o src/resources_rc.py 74 | poetry run pyinstaller ${{ github.event.inputs.spec }} 75 | - name: Compress (Windows) 76 | if: runner.os == 'Windows' 77 | run: tar.exe -acf AlMufarrigh-${{ runner.os }}-${{ matrix.architecture }}-${{ matrix.variant }}-portable.zip dist 78 | - name: Compress 79 | if: runner.os != 'Windows' 80 | run: zip -r9 AlMufarrigh-${{ runner.os }}-${{ matrix.architecture }}-${{ matrix.variant }}.zip dist/* 81 | - uses: actions/upload-artifact@v4 82 | with: 83 | name: AlMufarrigh-${{ runner.os }}-${{ matrix.architecture }}-${{ matrix.variant }} 84 | path: AlMufarrigh-${{ runner.os }}-${{ matrix.architecture }}-${{ matrix.variant }}* 85 | 86 | release: 87 | runs-on: ubuntu-latest 88 | needs: [ build ] 89 | steps: 90 | - uses: actions/checkout@v4 91 | - uses: actions/download-artifact@v4 92 | - name: Display structure of downloaded files 93 | run: ls -R 94 | - name: Release 95 | uses: ncipollo/release-action@v1.14.0 96 | with: 97 | allowUpdates: true 98 | commit: 'main' 99 | tag: ${{ github.event.inputs.version }} 100 | name: ${{ github.event.inputs.version }} 101 | artifacts: '*/*.zip' 102 | -------------------------------------------------------------------------------- /src/qml/components/ProcessPage.qml: -------------------------------------------------------------------------------- 1 | import "../utils/audiohelper.mjs" as AudioHelper 2 | import QtQuick 6.4 3 | import QtQuick.Controls 6.4 4 | import QtQuick.Dialogs 5 | import QtQuick.Layouts 6.4 6 | import "convert" 7 | 8 | Rectangle { 9 | id: progressPage 10 | 11 | property int index: 0 12 | property bool completionTextDisplayed: false 13 | 14 | signal sidebarButtonClicked(int index) 15 | signal clearAudioFiles() 16 | 17 | color: theme.background 18 | 19 | FontLoader { 20 | id: poppinsFontLoader 21 | 22 | source: theme.font.source 23 | } 24 | 25 | ListModel { 26 | id: audioFilesModel 27 | } 28 | 29 | Text { 30 | id: title 31 | 32 | color: theme.fontPrimary 33 | font.pixelSize: 40 34 | font.weight: Font.Bold 35 | horizontalAlignment: Text.AlignRight 36 | text: qsTr('تحــــويل مقــطع صوتي
إلى') 37 | Layout.alignment: Qt.AlignRight 38 | textFormat: Text.RichText 39 | 40 | anchors { 41 | top: parent.top 42 | right: parent.right 43 | topMargin: 64 44 | rightMargin: 75 45 | } 46 | 47 | } 48 | 49 | Image { 50 | id: textBackground 51 | 52 | source: mainWindow.isLightTheme ? "qrc:/text_background_light" : "qrc:/text_background_dark" 53 | width: 128 54 | 55 | anchors { 56 | top: parent.top 57 | right: title.right 58 | topMargin: title.anchors.topMargin + 48 59 | rightMargin: 60 60 | } 61 | 62 | } 63 | 64 | Text { 65 | color: "#fff" 66 | font.pixelSize: 40 67 | horizontalAlignment: Text.AlignRight 68 | text: qsTr('نـــص') 69 | font.weight: Font.Bold 70 | Layout.alignment: Qt.AlignRight 71 | textFormat: Text.RichText 72 | 73 | anchors { 74 | top: textBackground.top 75 | bottom: textBackground.bottom 76 | left: textBackground.left 77 | right: textBackground.right 78 | topMargin: 6 79 | rightMargin: 10 80 | } 81 | 82 | } 83 | 84 | Text { 85 | id: subtitle 86 | 87 | color: theme.fontSecondary 88 | font.pixelSize: 28 89 | text: qsTr("تلقائياً ومجاناً") 90 | font.weight: Font.Normal 91 | horizontalAlignment: Text.AlignRight 92 | Layout.alignment: Qt.AlignRight 93 | 94 | anchors { 95 | top: title.bottom 96 | right: title.right 97 | topMargin: 16 98 | } 99 | 100 | } 101 | 102 | CircularProgressBar { 103 | id: progressBar 104 | 105 | anchors.centerIn: parent 106 | width: 290 107 | height: 290 108 | foregroundColor: theme.primary 109 | backgroundColor: "#00000000" 110 | } 111 | // Add this property to track text display 112 | 113 | CustomButton { 114 | id: stopButton 115 | 116 | text: qsTr("توقــف") 117 | Layout.fillWidth: true 118 | onClicked: { 119 | backend.stop(); // Call a method in the backend to stop the process 120 | clearAudioFiles(); // Emit a signal to clear audioFilesModel in ConvertPage.qml 121 | stackLayout.currentIndex = 0; // Go back to ConvertPage.qml 122 | } 123 | 124 | anchors { 125 | horizontalCenter: parent.horizontalCenter 126 | top: progressBar.bottom 127 | topMargin: 16 128 | } 129 | 130 | } 131 | 132 | Text { 133 | // Component.onCompleted: { 134 | // Set the flag to true when the text is first displayed 135 | // completionTextDisplayed = true; 136 | // } 137 | 138 | id: completionText 139 | 140 | text: qsTr(". . . جاري تهيــئة الملـــــــفات . . .") 141 | color: theme.black 142 | font.pixelSize: 25 143 | visible: progressBar.value === 0 && !completionTextDisplayed 144 | 145 | anchors { 146 | horizontalCenter: parent.horizontalCenter 147 | top: stopButton.bottom 148 | topMargin: 8 149 | } 150 | 151 | } 152 | 153 | Connections { 154 | function onProgress(progress, remainingTime) { 155 | progressBar.value = progress; 156 | } 157 | 158 | function onFinish() { 159 | progressBar.value = 0; 160 | // Reset the completionTextDisplayed flag when the process finishes 161 | let completionTextDisplayed = true; 162 | } 163 | 164 | target: backend 165 | enabled: progressPage.visible 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/qml/components/convert/AcceptTasks.qml: -------------------------------------------------------------------------------- 1 | import "../../utils/audiohelper.mjs" as AudioHelper 2 | import QtQml 3 | import QtQuick 6.4 4 | import QtQuick.Controls 6.4 5 | import QtQuick.Controls.impl 6 | import QtQuick.Dialogs 7 | import QtQuick.Layouts 6.4 8 | 9 | DropArea { 10 | id: dropArea 11 | 12 | signal addedNewAudio(url audio) 13 | 14 | height: 350 15 | width: parent.width 16 | onDropped: (dragEvent) => { 17 | //Drag Drop logic 18 | console.log(dragEvent.urls); 19 | console.log(dropArea.data); 20 | for (let file in dragEvent.urls) { 21 | let path = file.toString(); 22 | let extension = path.substring(path.lastIndexOf(".") + 1).toLowerCase(); 23 | console.log(extension); 24 | if (extension === "mp3" || extension === "m4a") { 25 | // File is an audio file, process it 26 | console.log("Audio file dropped:", file); 27 | addedNewAudio(file); 28 | } else { 29 | // File is not supported, show an error message or ignore it 30 | //TODO add warning pop 31 | console.log("Unsupported file format:", file); 32 | } 33 | } 34 | } 35 | 36 | Rectangle { 37 | anchors.fill: parent 38 | radius: 24 39 | color: theme.card 40 | 41 | Canvas { 42 | anchors.fill: parent 43 | onPaint: { 44 | const ctx = getContext("2d"); 45 | const borderRadius = 24; 46 | const halfBorderWidth = 1.5; // Half the desired border width for proper positioning 47 | ctx.strokeStyle = theme.primary; 48 | ctx.lineWidth = 3; 49 | ctx.setLineDash([5, 5]); 50 | ctx.beginPath(); 51 | ctx.moveTo(borderRadius + halfBorderWidth, halfBorderWidth); // Start slightly offset to correctly position the border 52 | ctx.lineTo(width - borderRadius - halfBorderWidth, halfBorderWidth); 53 | ctx.arcTo(width - halfBorderWidth, halfBorderWidth, width - halfBorderWidth, borderRadius + halfBorderWidth, borderRadius); 54 | ctx.lineTo(width - halfBorderWidth, height - borderRadius - halfBorderWidth); 55 | ctx.arcTo(width - halfBorderWidth, height - halfBorderWidth, width - borderRadius - halfBorderWidth, height - halfBorderWidth, borderRadius); 56 | ctx.lineTo(borderRadius + halfBorderWidth, height - halfBorderWidth); 57 | ctx.arcTo(halfBorderWidth, height - halfBorderWidth, halfBorderWidth, height - borderRadius - halfBorderWidth, borderRadius); 58 | ctx.lineTo(halfBorderWidth, borderRadius + halfBorderWidth); 59 | ctx.arcTo(halfBorderWidth, halfBorderWidth, borderRadius + halfBorderWidth, halfBorderWidth, borderRadius); 60 | ctx.closePath(); // Close the path for better rendering 61 | ctx.stroke(); 62 | } 63 | } 64 | 65 | } 66 | 67 | ColumnLayout { 68 | anchors.centerIn: parent 69 | spacing: 20 70 | 71 | IconImage { 72 | source: "qrc:/add_task" 73 | Layout.alignment: Qt.AlignCenter 74 | width: 100 75 | height: 100 76 | color: theme.fontThirty 77 | } 78 | 79 | RowLayout { 80 | Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter 81 | 82 | Text { 83 | text: qsTr("كمبيوترك") 84 | color: theme.primary 85 | font.pixelSize: 22 86 | font.weight: Font.Medium 87 | } 88 | 89 | Text { 90 | text: qsTr("اســحب وأفلت هنا أو اختر من") 91 | color: theme.fontSecondary 92 | font.pixelSize: 22 93 | font.weight: Font.Medium 94 | } 95 | 96 | } 97 | 98 | Text { 99 | text: qsTr("أو الصق رابطاً") 100 | font.pixelSize: 22 101 | font.weight: Font.Medium 102 | color: theme.fontThirty 103 | Layout.alignment: Qt.AlignCenter 104 | } 105 | 106 | CustomButton { 107 | implicitWidth: 148 108 | Layout.alignment: Qt.AlignCenter 109 | text: qsTr("+ إضـــافة") 110 | onClicked: fileDialog.open() 111 | } 112 | 113 | FileDialog { 114 | id: fileDialog 115 | 116 | title: qsTr("Please choose a file") 117 | nameFilters: ["Audio Files (*.mp3 *.wav *.m4a *.ogg)"] 118 | onAccepted: { 119 | addedNewAudio(selectedFile); 120 | } 121 | onRejected: { 122 | console.log("Canceled"); 123 | } 124 | } 125 | 126 | } 127 | 128 | PasteConfirm { 129 | id: pasteConfirm 130 | 131 | onPasteConfirmed: addedNewAudio(urlStr) 132 | } 133 | 134 | Connections { 135 | function onTextChanged() { 136 | let text = clipboard.getClipboardText(); 137 | if (AudioHelper.isYoutubeLink(text)) 138 | pasteConfirm.openWithUrl(text); 139 | 140 | } 141 | 142 | target: clipboard 143 | enabled: parent.visible 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/qml/components/ConvertPage.qml: -------------------------------------------------------------------------------- 1 | import "../utils/audiohelper.mjs" as AudioHelper 2 | import QtQuick 6.4 3 | import QtQuick.Controls 6.4 4 | import QtQuick.Dialogs 5 | import QtQuick.Layouts 6.4 6 | import "convert" 7 | 8 | Rectangle { 9 | id: root 10 | //audioUrls must be in jsonstring format 11 | signal convertRequested(list urls) 12 | 13 | color: theme.background 14 | 15 | FontLoader { 16 | id: poppinsFontLoader 17 | 18 | source: theme.font.source 19 | } 20 | 21 | property ListModel audioFilesModel: ListModel { 22 | id: audioFilesModel 23 | } 24 | function clearAudioFiles() { 25 | audioFilesModel.clear(); 26 | } 27 | 28 | Text { 29 | id: title 30 | 31 | color: theme.fontPrimary 32 | font.pixelSize: 40 33 | font.weight: Font.Bold 34 | horizontalAlignment: Text.AlignRight 35 | text: qsTr('تحــــويل مقــطع صوتي
إلى') 36 | Layout.alignment: Qt.AlignRight 37 | textFormat: Text.RichText 38 | 39 | anchors { 40 | top: parent.top 41 | right: parent.right 42 | topMargin: 64 43 | rightMargin: 75 44 | } 45 | } 46 | 47 | Image { 48 | id: textBackground 49 | 50 | source: mainWindow.isLightTheme ? "qrc:/text_background_light" : "qrc:/text_background_dark" 51 | width: 128 52 | 53 | anchors { 54 | top: parent.top 55 | right: title.right 56 | topMargin: title.anchors.topMargin + 48 57 | rightMargin: 60 58 | } 59 | } 60 | 61 | Text { 62 | color: "#fff" 63 | font.pixelSize: 40 64 | horizontalAlignment: Text.AlignRight 65 | text: qsTr('نـــص') 66 | font.weight: Font.Bold 67 | Layout.alignment: Qt.AlignRight 68 | textFormat: Text.RichText 69 | 70 | anchors { 71 | top: textBackground.top 72 | bottom: textBackground.bottom 73 | left: textBackground.left 74 | right: textBackground.right 75 | topMargin: 6 76 | rightMargin: 10 77 | } 78 | } 79 | 80 | Text { 81 | id: subtitle 82 | 83 | color: theme.fontSecondary 84 | font.pixelSize: 28 85 | text: qsTr("تلقائياً ومجاناً") 86 | font.weight: Font.Normal 87 | horizontalAlignment: Text.AlignRight 88 | Layout.alignment: Qt.AlignRight 89 | 90 | anchors { 91 | top: title.bottom 92 | right: title.right 93 | topMargin: 16 94 | } 95 | } 96 | 97 | AcceptTasks { 98 | id: acceptTasks 99 | 100 | onAddedNewAudio: audio => { 101 | audioFilesModel.append({ 102 | "file": audio 103 | }); 104 | } 105 | 106 | anchors { 107 | top: title.bottom 108 | right: subtitle.left 109 | left: parent.left 110 | rightMargin: 72 111 | leftMargin: 72 112 | } 113 | } 114 | 115 | Rectangle { 116 | id: audioDeck 117 | 118 | visible: audioFilesModel.count > 0 119 | width: parent.width 120 | height: 200 121 | color: theme.card 122 | radius: 10 123 | 124 | anchors { 125 | top: acceptTasks.bottom 126 | right: acceptTasks.right 127 | left: acceptTasks.left 128 | topMargin: 16 129 | } 130 | 131 | ListView { 132 | anchors.fill: parent 133 | anchors.margins: 10 134 | model: audioFilesModel 135 | spacing: 10 136 | 137 | ScrollBar.vertical: ScrollBar { 138 | policy: ScrollBar.AsNeeded 139 | } 140 | 141 | delegate: AudioTask { 142 | fileName: AudioHelper.extractTextIdentifier(modelData.toString()) 143 | onRemoveAudioRequested: { 144 | audioFilesModel.remove(index); // Remove the audio file from the model 145 | } 146 | } 147 | } 148 | } 149 | 150 | RowLayout { 151 | visible: audioFilesModel.count > 0 152 | Layout.fillWidth: true 153 | layoutDirection: Qt.RightToLeft 154 | height: 50 155 | 156 | anchors { 157 | top: audioDeck.bottom 158 | right: audioDeck.right 159 | left: audioDeck.left 160 | topMargin: 16 161 | } 162 | 163 | CustomButton { 164 | text: qsTr("البــــدء") 165 | Layout.fillWidth: true 166 | onClicked: { 167 | var listData = []; 168 | for (var i = 0; i < audioFilesModel.count; i++) { 169 | var item = audioFilesModel.get(i); 170 | listData.push(item.file); 171 | } 172 | root.convertRequested(listData); 173 | root.startConversionClicked(); 174 | } 175 | } 176 | 177 | CustomButton { 178 | text: qsTr("إلغــاء") 179 | backColor: theme.card 180 | Layout.fillWidth: true 181 | onClicked: audioFilesModel.clear() 182 | } 183 | 184 | 185 | } 186 | 187 | Connections { 188 | target: backend 189 | enabled: root.visible 190 | 191 | function onFinish() { 192 | audioFilesModel.clear(); 193 | } 194 | } 195 | signal startConversionClicked(); 196 | } 197 | -------------------------------------------------------------------------------- /src/domain/backend.py: -------------------------------------------------------------------------------- 1 | """Backend that interacts with tafrigh.""" 2 | 3 | import json 4 | import multiprocessing 5 | from collections import OrderedDict 6 | from pathlib import Path 7 | from platform import system 8 | from subprocess import Popen 9 | from typing import Any 10 | 11 | from PySide6.QtCore import Property, QObject, QThreadPool, Signal, Slot 12 | from PySide6.QtWidgets import QMessageBox 13 | from tafrigh import Config, farrigh 14 | 15 | from domain.config import AppConfig, CaseSensitiveConfigParser 16 | from domain.progress import Progress 17 | from domain.threadpool import Worker, WorkerSignals 18 | from domain.token_manager import TokenManager 19 | 20 | 21 | def replace_path(path: str) -> str: 22 | """Remove file:/// from the path, handles both windows and linux paths.""" 23 | if system() == "Windows": 24 | return path.replace("file:///", "") 25 | return path.replace("file://", "") 26 | 27 | 28 | def get_path(path: str) -> str: 29 | """Get the path with file:/// prefix for windows and file:// for linux.""" 30 | return f"file:///{path}" if system() == "Windows" else f"file://{path}" 31 | 32 | 33 | # BACKEND 34 | class Backend(QObject): 35 | """Backend object.""" 36 | 37 | result = Signal(dict) 38 | progress = Signal(int, int) 39 | error = Signal(tuple) 40 | finish = Signal() 41 | 42 | def __init__(self, parent: QObject | None = None) -> None: 43 | """Initialize backend object.""" 44 | super().__init__(parent=parent) 45 | self.signals = WorkerSignals() 46 | self.threadpool = QThreadPool() 47 | self.token_manager = TokenManager() 48 | self._is_running = False 49 | self._urls: list[str] = [] 50 | self._stop_flag = False 51 | 52 | @Slot() 53 | def stop(self) -> None: 54 | """Set the stop flag to True.""" 55 | self._stop_flag = True 56 | self.finish.emit() 57 | 58 | def on_error(self, error: tuple[str, int, str]) -> None: 59 | error_str, error_code, error_message = error 60 | self.error.emit(error) 61 | 62 | QMessageBox.warning(None, "Warning", str(error_code)) 63 | 64 | self._is_running = False 65 | 66 | def on_result(self, result: dict[str, Any]) -> None: 67 | self.result.emit(result) 68 | 69 | def on_progress(self, progress: Progress) -> None: 70 | self.progress.emit(progress.value, progress.remaining_time) 71 | 72 | def on_finish(self) -> None: 73 | self.finish.emit() 74 | 75 | @Slot() 76 | def start(self) -> None: 77 | if system() != "Linux": 78 | multiprocessing.freeze_support() 79 | 80 | worker = Worker(func=self.run) 81 | worker.signals.finished.connect(self.on_finish) 82 | worker.signals.progress.connect(self.on_progress) 83 | worker.signals.error.connect(self.on_error) 84 | worker.signals.result.connect(self.on_result) 85 | self.threadpool.start(worker) 86 | 87 | @Property(list) 88 | def urls(self) -> list[str]: 89 | return self._urls 90 | 91 | @urls.setter # type: ignore[no-redef] 92 | def urls(self, value: list[str]): 93 | self._urls = [replace_path(x) for x in value] 94 | 95 | def run(self, *args: Any, **kwargs: Any) -> Any: 96 | app_config: AppConfig = CaseSensitiveConfigParser.read_config() 97 | 98 | config = Config( 99 | input=Config.Input( 100 | urls_or_paths=self.urls, 101 | skip_if_output_exist=True, 102 | download_retries=3, 103 | playlist_items="", 104 | verbose=False, 105 | ), 106 | whisper=Config.Whisper( 107 | model_name_or_path=app_config.whisper_model, 108 | task="transcribe", 109 | language=app_config.convert_language, 110 | use_faster_whisper=True, 111 | beam_size=5, 112 | ct2_compute_type="default", 113 | ), 114 | wit=Config.Wit( 115 | wit_client_access_tokens=[app_config.wit_convert_key] 116 | if app_config.is_wit_engine 117 | else [""], 118 | max_cutting_duration=int(app_config.max_part_length), 119 | ), 120 | output=Config.Output( 121 | min_words_per_segment=app_config.word_count, 122 | save_files_before_compact=False, 123 | save_yt_dlp_responses=app_config.download_json, 124 | output_sample=0, 125 | output_formats=app_config.get_output_formats(), 126 | output_dir=replace_path(app_config.save_location or get_path(str(Path.cwd()))), 127 | ), 128 | ) 129 | 130 | return farrigh(config) 131 | 132 | @Slot(str) 133 | @staticmethod 134 | def open_folder(path: str) -> None: 135 | if system() == "Windows": 136 | from os import startfile # type: ignore[attr-defined] 137 | 138 | startfile(path) # noqa: S606 139 | elif system() == "Darwin": 140 | Popen(["open", path], shell=False) # noqa: S603, S607 141 | else: 142 | Popen(["xdg-open", path], shell=False) # noqa: S603, S607 143 | 144 | @Slot(str, str) 145 | def save_convert_token(self, language: str, token: str) -> None: 146 | tokens = self.token_manager.read_tokens() 147 | tokens[language] = token 148 | self.token_manager.save_tokens(tokens) 149 | 150 | @Slot(str, result=str) 151 | def get_convert_token(self, language: str) -> str | None: 152 | tokens: dict[str, str] = self.token_manager.read_tokens() 153 | return tokens.get(language) 154 | 155 | @Slot(result=list) 156 | def get_languages(self) -> list[dict[str, str]]: 157 | root_path = Path(__file__).parent.parent 158 | languages_path = root_path / "resources/languages.json" 159 | if not languages_path.exists(): 160 | return [] 161 | languages_dict = json.loads( 162 | languages_path.read_text(encoding="utf-8"), 163 | object_pairs_hook=OrderedDict, 164 | ) 165 | return [{"text": text, "value": value} for value, text in languages_dict.items()] 166 | -------------------------------------------------------------------------------- /src/qml/components/SettingsPage.qml: -------------------------------------------------------------------------------- 1 | import QtCore 2 | import QtQuick 6.4 3 | import QtQuick.Controls 6.4 4 | import QtQuick.Controls.impl 5 | import QtQuick.Dialogs 6 | import QtQuick.Layouts 6.4 7 | import "custom" 8 | import "settings" 9 | 10 | Rectangle { 11 | id: root 12 | 13 | property bool isWitEngine: true 14 | property alias saveLocation: saveLocation.value 15 | 16 | signal themeChanged(bool state) 17 | 18 | color: theme.background 19 | Component.onCompleted: { 20 | backend.get_languages().forEach(function(language) { 21 | convertLanguage.dropdownModel.append(language); 22 | }); 23 | } 24 | 25 | ColumnLayout { 26 | spacing: 10 27 | anchors.left: parent.left 28 | anchors.right: parent.right 29 | anchors.top: parent.top 30 | anchors.bottom: copyRights.top 31 | anchors.rightMargin: 24 32 | anchors.topMargin: 48 33 | anchors.leftMargin: 24 34 | anchors.bottomMargin: 48 35 | 36 | SettingsDropDown { 37 | id: convertLanguage 38 | 39 | property string value: convertLanguage.selectedValue 40 | 41 | onChangedSelection: (index, selected) => { 42 | value = selected; 43 | witConvertKey.value = backend.get_convert_token(selected); 44 | } 45 | iconSource: "qrc:/convert_language" 46 | labelText: qsTr("لــغة التحـويل") 47 | currentIndex: 0 48 | } 49 | 50 | SettingsDropDown { 51 | id: convertEngine 52 | 53 | property string value: convertEngine.selectedValue 54 | 55 | iconSource: "qrc:/convert_engine" 56 | labelText: qsTr("محرك التحـويل") 57 | onChangedSelection: (index, selected) => { 58 | isWitEngine = index === 0; 59 | value = selected; 60 | } 61 | 62 | dropdownModel: ListModel { 63 | ListElement { 64 | text: "Wit.ai" 65 | value: "Wit" 66 | } 67 | 68 | ListElement { 69 | text: "Whisper" 70 | value: "Whisper" 71 | } 72 | 73 | } 74 | 75 | } 76 | 77 | SettingsDropDown { 78 | id: whisperModel 79 | 80 | property string value: whisperModel.selectedValue 81 | 82 | visible: !root.isWitEngine 83 | iconSource: "qrc:/select_model" 84 | labelText: qsTr("تحديد النموذج") 85 | onChangedSelection: (index, selected) => { 86 | value = selected; 87 | } 88 | 89 | dropdownModel: ListModel { 90 | ListElement { 91 | text: qsTr("أساسي") 92 | value: "base" 93 | } 94 | 95 | ListElement { 96 | text: qsTr("صغير") 97 | value: "small" 98 | } 99 | 100 | ListElement { 101 | text: qsTr("متوسط") 102 | value: "medium" 103 | } 104 | 105 | ListElement { 106 | text: qsTr("نحيف (أقل دقة)") 107 | value: "tiny" 108 | } 109 | 110 | ListElement { 111 | text: qsTr("كبير (أفضل دقة)") 112 | value: "large-v2" 113 | } 114 | 115 | } 116 | 117 | } 118 | 119 | SettingsItem { 120 | id: witConvertKey 121 | 122 | property alias value: inputText.text 123 | 124 | visible: root.isWitEngine 125 | iconSource: "qrc:/key" 126 | labelText: qsTr("مفتاح التحـويل") 127 | 128 | TextField { 129 | id: inputText 130 | 131 | color: theme.fontPrimary 132 | implicitWidth: parent.width * (2 / 3) 133 | implicitHeight: 40 134 | font.pixelSize: 16 135 | selectByMouse: true // Allows selecting the text with the mouse 136 | inputMethodHints: Qt.ImhDigitsOnly // Restricts input to digits only 137 | onTextChanged: { 138 | const token = inputText.text; 139 | const language = convertLanguage.value; 140 | backend.save_convert_token(language, token); 141 | } 142 | 143 | background: Rectangle { 144 | color: theme.field 145 | border.color: theme.stroke 146 | border.width: 1 147 | radius: 8 148 | } 149 | // Sets the font size to a small value 150 | 151 | } 152 | 153 | } 154 | 155 | SettingsItem { 156 | id: wordCount 157 | 158 | property alias value: countInput.text 159 | 160 | iconSource: "qrc:/word_count" 161 | labelText: qsTr("عدد كلمات الجزء") 162 | 163 | TextField { 164 | id: countInput 165 | 166 | implicitHeight: 40 167 | text: "0" 168 | color: theme.fontPrimary 169 | font.pixelSize: 16 // Sets the font size to a small value 170 | selectByMouse: true // Allows selecting the text with the mouse 171 | inputMethodHints: Qt.ImhDigitsOnly // Restricts input to digits only 172 | width: Math.max(countInput.contentWidth + 4, 40) 173 | 174 | background: Rectangle { 175 | color: theme.field 176 | border.color: theme.stroke 177 | border.width: 1 178 | radius: 8 179 | } 180 | 181 | // Limits input to positive integers between 0 and 999 182 | validator: IntValidator { 183 | bottom: 0 184 | top: 999 185 | } 186 | 187 | } 188 | 189 | } 190 | 191 | SettingsItem { 192 | id: maxPartLength 193 | 194 | property alias value: slider.value 195 | 196 | visible: root.isWitEngine 197 | iconSource: "qrc:/part_max" 198 | labelText: qsTr("أقصى مدة للجزء") 199 | 200 | Slider { 201 | id: slider 202 | 203 | from: 3 204 | to: 17 205 | implicitWidth: parent.width / 3 206 | } 207 | 208 | } 209 | 210 | SettingsItem { 211 | id: dropEmptyParts 212 | 213 | property alias checked: checkbox.checked 214 | 215 | visible: root.isWitEngine 216 | Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter 217 | iconSource: "qrc:/drop_empty" 218 | labelText: qsTr("إسقاط الأجزاء الفارغة") 219 | 220 | CustomCheckBox { 221 | id: checkbox 222 | } 223 | 224 | } 225 | 226 | SettingsItem { 227 | id: exportFormats 228 | 229 | iconSource: "qrc:/export" 230 | labelText: qsTr("صيغ المخرجات") 231 | 232 | RowLayout { 233 | spacing: 10 234 | Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter 235 | 236 | CustomCheckBox { 237 | id: srt 238 | 239 | text: "srt" 240 | checked: true 241 | } 242 | 243 | CustomCheckBox { 244 | id: txt 245 | 246 | text: "txt" 247 | } 248 | 249 | CustomCheckBox { 250 | id: vtt 251 | 252 | text: "vtt" 253 | } 254 | 255 | } 256 | 257 | } 258 | 259 | SettingsItem { 260 | id: saveLocation 261 | 262 | property string value 263 | 264 | iconSource: "qrc:/folder" 265 | labelText: qsTr("مجلـد الحفــظ") 266 | 267 | Rectangle { 268 | id: openFileDialogButton 269 | 270 | width: Math.max(120, path.contentWidth + 64) 271 | height: 40 272 | radius: 8 273 | border.color: theme.stroke 274 | border.width: 2 275 | color: theme.field 276 | 277 | IconImage { 278 | source: "qrc:/arrow_left" 279 | color: theme.fontPrimary 280 | anchors.left: parent.left 281 | y: 8 282 | anchors.leftMargin: 8 283 | Layout.alignment: Qt.AlignVCenter 284 | } 285 | 286 | Text { 287 | id: path 288 | 289 | text: "/ " + saveLocation.value.split("/").slice(-1) 290 | color: theme.fontPrimary 291 | font.weight: Font.Medium 292 | font.pixelSize: 22 293 | anchors.centerIn: parent 294 | horizontalAlignment: Text.AlignHCenter 295 | verticalAlignment: Text.AlignVCenter 296 | x: 8 297 | } 298 | 299 | MouseArea { 300 | anchors.fill: parent 301 | onClicked: { 302 | folderDialog.open(); 303 | } 304 | } 305 | 306 | FolderDialog { 307 | id: folderDialog 308 | 309 | selectedFolder: saveLocation.value 310 | title: qsTr("Please choose a file") 311 | onAccepted: { 312 | saveLocation.value = selectedFolder; 313 | } 314 | onRejected: { 315 | console.log("Canceled"); 316 | } 317 | } 318 | 319 | } 320 | 321 | } 322 | 323 | SettingsItem { 324 | id: jsonLoad 325 | 326 | property alias checked: jsonCheck.checked 327 | 328 | iconSource: "qrc:/download" 329 | labelText: qsTr("تحميل ملف json") 330 | 331 | CustomCheckBox { 332 | id: jsonCheck 333 | } 334 | 335 | } 336 | 337 | SettingsItem { 338 | iconSource: "qrc:/theme" 339 | labelText: qsTr("الثـــيـــم") 340 | 341 | Switch { 342 | id: themeSwitch 343 | 344 | checked: !mainWindow.isLightTheme 345 | onToggled: { 346 | themeChanged(checked); 347 | } 348 | } 349 | 350 | } 351 | 352 | } 353 | 354 | ColumnLayout { 355 | id: copyRights 356 | 357 | spacing: 0 358 | anchors.bottom: parent.bottom 359 | anchors.right: parent.right 360 | anchors.left: parent.left 361 | anchors.bottomMargin: 28 362 | Layout.alignment: Qt.AlignHCenter 363 | 364 | Text { 365 | text: "Copyright © 2022-2023 almufaragh.com." 366 | horizontalAlignment: Text.AlignHCenter 367 | Layout.alignment: Qt.AlignHCenter 368 | color: theme.fontPrimary 369 | } 370 | 371 | Text { 372 | text: qsTr("الإصدار 1.0.6") 373 | Layout.alignment: Qt.AlignHCenter 374 | horizontalAlignment: Text.AlignHCenter 375 | color: theme.fontPrimary 376 | } 377 | 378 | } 379 | 380 | Settings { 381 | id: settings 382 | 383 | property alias isWitEngine: root.isWitEngine 384 | property alias downloadJson: jsonCheck.checked 385 | property alias saveLocation: saveLocation.value 386 | property alias exportSrt: srt.checked 387 | property alias exportTxt: txt.checked 388 | property alias exportVtt: vtt.checked 389 | property alias dropEmptyParts: dropEmptyParts.checked 390 | property alias maxPartLength: maxPartLength.value 391 | property alias wordCount: wordCount.value 392 | property alias witConvertKey: witConvertKey.value 393 | property alias whisperModel: whisperModel.value 394 | property alias convertEngine: convertEngine.value 395 | property alias convertLanguage: convertLanguage.value 396 | 397 | category: "config" 398 | location: "file:settings.ini" 399 | } 400 | 401 | Settings { 402 | property alias whisperModelIndex: whisperModel.currentIndex 403 | property alias convertEngineIndex: convertEngine.currentIndex 404 | property alias convertLanguageIndex: convertLanguage.currentIndex 405 | 406 | category: "app" 407 | location: "file:settings.ini" 408 | } 409 | 410 | Connections { 411 | target: backend 412 | enabled: root.visible 413 | } 414 | 415 | } 416 | --------------------------------------------------------------------------------