├── 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 |
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 |
4 |
--------------------------------------------------------------------------------
/src/resources/icons/close_circle.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/resources/icons/audio.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # المفرّغ
2 |
3 | [](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 |
6 |
--------------------------------------------------------------------------------
/src/resources/icons/export.svg:
--------------------------------------------------------------------------------
1 |
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 |
5 |
--------------------------------------------------------------------------------
/src/resources/icons/part_max.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
5 |
--------------------------------------------------------------------------------
/src/resources/icons/select_model.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/resources/icons/convert_language.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/resources/icons/quit.svg:
--------------------------------------------------------------------------------
1 |
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 |
6 |
--------------------------------------------------------------------------------
/src/resources/icons/convert_engine.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
5 |
--------------------------------------------------------------------------------
/src/resources/icons/video.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/resources/icons/word_count.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/resources/icons/folder.svg:
--------------------------------------------------------------------------------
1 |
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 |
5 |
--------------------------------------------------------------------------------
/src/resources/icons/settings_unselected.svg:
--------------------------------------------------------------------------------
1 |
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 |
6 |
--------------------------------------------------------------------------------
/src/resources/icons/settings_selected.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
--------------------------------------------------------------------------------