diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1d33295..70a852e 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -15,7 +15,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} tests: - runs-on: macOS-latest + runs-on: macos-11 steps: - uses: actions/checkout@v2 diff --git a/.periphery.yml b/.periphery.yml deleted file mode 100644 index 703d52f..0000000 --- a/.periphery.yml +++ /dev/null @@ -1,8 +0,0 @@ -project: Prayer.xcodeproj -schemes: [App] -targets: [App, Tests, UITests] -report_exclude: - - App/Generated/SwiftGen/*.swift - - App/SupportingFiles/*.swift -retain_public: false -quiet: true diff --git a/.projlint.yml b/.projlint.yml deleted file mode 100644 index 7380a96..0000000 --- a/.projlint.yml +++ /dev/null @@ -1,173 +0,0 @@ -shared_variables: - rightholder: Flinesoft - project_name: Prayer - -rules: - - xcode_build_phases: - project_path: <:project_name:>.xcodeproj - target_name: App - run_scripts: - BartyCrouch: | - if which bartycrouch > /dev/null; then - bartycrouch update -x - bartycrouch lint -x - else - echo "warning: BartyCrouch not installed, download it from https://github.com/Flinesoft/BartyCrouch" - fi - - SwiftGen: | - if which swiftgen > /dev/null; then - swiftgen - else - echo "warning: SwiftGen not installed, download it from https://github.com/AliSoftware/SwiftGen" - fi - - SwiftLint: | - if which swiftlint > /dev/null; then - swiftlint --quiet - else - echo "warning: SwiftLint not installed, download it from https://github.com/realm/SwiftLint" - fi - - ProjLint: | - if which projlint > /dev/null; then - projlint lint --xcode --timeout 2 --ignore-network-errors - else - echo "warning: ProjLint not installed, download it from https://github.com/JamitLabs/ProjLint" - fi - - - xcode_project_navigator: - project_path: <:project_name:>.xcodeproj - sorted: - - App/Sources - - App/Resources - - App/SupportingFiles - - App/Generated - - Tests/Sources - - Tests/Resources - - Tests/SupportingFiles - - UITests/Sources - - UITests/Resources - - UITests/SupportingFiles - inner_group_order: [] - structure: - - App: - - Sources: - - AppDelegate.swift - - Globals: - - Extensions - - Resources: - - Colors.xcassets - - Images.xcassets - - Localizable.strings - - Fonts - - SupportingFiles: - - App.entitlements - - BartyCrouch.swift - - LaunchScreen.storyboard - - Info.plist - - InfoPlist.strings - - Generated: - - SwiftGen: - - Assets.swift - - Storyboards.swift - - Strings.swift - - Tests: - - Sources - - Resources - - SupportingFiles: - - Info.plist - - UITests: - - Sources - - Resources - - SupportingFiles: - - Info.plist - - Extensions - - RootFiles: - - .bartycrouch.toml - - .envrc - - .gitignore - - .periphery.yml - - .projlint.yml - - .swiftlint.yml - - beak.swift - - Brewfile - - Cartfile - - Cartfile.resolved - - SwiftGen-xcassets.stencil - - swiftgen.yml - - Frameworks: - - Carthage: - - App - - Tests - - Local: - - App - - Tests - - Products - - file_content_regex: - matching_all: - Cartfile: - - "#\\s*[^\\s]+" # Ensure dependencies are commented - - HandySwift - - HandyUIKit - - MungoHealer - - SwiftyBeaver - - SwiftyUserDefaults - .envrc: - - "PATH_add ./Scripts/SymLinks" - .gitignore: - - Scripts/SymLinks - - Carthage/Build - - .build - Brewfile: - - beak - - direnv - not_matching_all: - Cartfile: # Moya already includes Alamofire, prevent redundancy - - Alamofire - - Moya - - file_content_template: - matching: - .swiftlint.yml: - template_url: "https://raw.githubusercontent.com/JamitLabs/ProjLintTemplates/master/JamitLabs/App/SwiftLint.stencil" - parameters: - rightholder: <:rightholder:> - .projlint.yml: - template_url: "https://raw.githubusercontent.com/JamitLabs/ProjLintTemplates/master/JamitLabs/App/ProjLint.stencil" - parameters: - rightholder: <:rightholder:> - project_name: <:project_name:> - - file_existence: - existing_paths: - - .bartycrouch.toml - - .envrc - - .gitignore - - .periphery.yml - - .projlint.yml - - .swiftlint.yml - - beak.swift - - Brewfile - - Cartfile - - Cartfile.resolved - - README.md - - SwiftGen-xcassets.stencil - - swiftgen.yml - - <:project_name:>.xcodeproj/xcshareddata/IDETemplateMacros.plist - - App/Sources/AppDelegate.swift - - App/Sources/Globals/Branding.swift - - App/Sources/Globals/ErrorHandler.swift - - App/Sources/Globals/Logger.swift - - App/Generated/SwiftGen/Assets.swift - - App/Generated/SwiftGen/Storyboards.swift - - App/Generated/SwiftGen/Strings.swift - - App/SupportingFiles/Info.plist - - App/Resources/de.lproj/Localizable.strings - - App/Resources/en.lproj/Localizable.strings - - App/Resources/Colors.xcassets/Contents.json - - App/Resources/Images.xcassets/AppIcon.appiconset/Contents.json - - Scripts/ci.swift - - Scripts/deps.swift - - Scripts/project.swift - - Scripts/tools.swift - - Tests/SupportingFiles/Info.plist - - UITests/SupportingFiles/Info.plist diff --git a/.swift-format b/.swift-format index e77ea6f..2325ba3 100644 --- a/.swift-format +++ b/.swift-format @@ -3,7 +3,7 @@ "lineBreakBeforeControlFlowKeywords": true, "lineBreakBeforeEachArgument": true, "lineBreakBeforeEachGenericRequirement": true, - "lineLength": 120, + "lineLength": 140, "prioritizeKeepingFunctionOutputTogether": true, "rules": { "NeverUseImplicitlyUnwrappedOptionals": false, diff --git a/App/Generated/SwiftGen/Assets.swift b/App/Generated/SwiftGen/Assets.swift index 0dbdd10..d2c3540 100644 --- a/App/Generated/SwiftGen/Assets.swift +++ b/App/Generated/SwiftGen/Assets.swift @@ -7,7 +7,6 @@ import UIKit.UIImage #endif -// MARK: - Asset Catalogs internal typealias Colors = Asset.Colors internal typealias Images = Asset.Images @@ -27,6 +26,5 @@ internal enum Asset { } } -// MARK: - Implementation Details private final class BundleToken {} diff --git a/App/Generated/SwiftGen/Strings.swift b/App/Generated/SwiftGen/Strings.swift index 2bf7b22..ef5e086 100644 --- a/App/Generated/SwiftGen/Strings.swift +++ b/App/Generated/SwiftGen/Strings.swift @@ -11,6 +11,17 @@ import Foundation // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces internal enum L10n { + internal enum AudioMode { + /// Sound for position changes + internal static let movementSound = L10n.tr("Localizable", "AUDIO_MODE.MOVEMENT_SOUND") + /// Both (sound + voice) + internal static let movementSoundAndSpeechSynthesizer = L10n.tr("Localizable", "AUDIO_MODE.MOVEMENT_SOUND_AND_SPEECH_SYNTHESIZER") + /// No audio (muted) + internal static let `none` = L10n.tr("Localizable", "AUDIO_MODE.NONE") + /// Reading computer voice + internal static let speechSynthesizer = L10n.tr("Localizable", "AUDIO_MODE.SPEECH_SYNTHESIZER") + } + internal enum PrayerView { internal enum Countdown { /// Countdown @@ -176,13 +187,51 @@ internal enum L10n { /// Settings internal static let title = L10n.tr("Localizable", "SETTINGS.TITLE") internal enum AppSection { + /// The language of the app determines the language of the texts shown during a prayer. Apple provides an app-specific language setting in the Settings app. Please use it to change the language of this app and the prayers. + internal static let footer = L10n.tr("Localizable", "SETTINGS.APP_SECTION.FOOTER") /// App Settings internal static let title = L10n.tr("Localizable", "SETTINGS.APP_SECTION.TITLE") internal enum ChangeLanguageButton { - /// Change app language in ⚙️ Settings app + /// Change language in ⚙️ Settings internal static let title = L10n.tr("Localizable", "SETTINGS.APP_SECTION.CHANGE_LANGUAGE_BUTTON.TITLE") } } + internal enum Audio { + internal enum OutputDevice { + /// Select output device (via AirPlay icon): + internal static let title = L10n.tr("Localizable", "SETTINGS.AUDIO.OUTPUT_DEVICE.TITLE") + } + internal enum SpeechSynthesizer { + /// Pitch multiplicator + internal static let pitchMultiplier = L10n.tr("Localizable", "SETTINGS.AUDIO.SPEECH_SYNTHESIZER.PITCH_MULTIPLIER") + /// Speech rate + internal static let speechRate = L10n.tr("Localizable", "SETTINGS.AUDIO.SPEECH_SYNTHESIZER.SPEECH_RATE") + /// Voice + internal static let voice = L10n.tr("Localizable", "SETTINGS.AUDIO.SPEECH_SYNTHESIZER.VOICE") + } + } + internal enum AudioSpeedSection { + /// Choose one of the two available audio modes, or both or none. The first only plays a short sound when changing positions during a prayer. The second will instead read out loud the full prayer text so you can just listen. This also works when the device is locked in your pocket. This can for example be useful when you want to do your prayers in your preferred language without others in the same Prayer room noticing it. + internal static let footer = L10n.tr("Localizable", "SETTINGS.AUDIO_SPEED_SECTION.FOOTER") + /// Audio & Speed Settings + internal static let title = L10n.tr("Localizable", "SETTINGS.AUDIO_SPEED_SECTION.TITLE") + internal enum AudioMode { + /// Audio Mode + internal static let title = L10n.tr("Localizable", "SETTINGS.AUDIO_SPEED_SECTION.AUDIO_MODE.TITLE") + } + internal enum ChangingText { + /// Quran sura speed + internal static let title = L10n.tr("Localizable", "SETTINGS.AUDIO_SPEED_SECTION.CHANGING_TEXT.TITLE") + } + internal enum FixedTexts { + /// Other texts speed + internal static let title = L10n.tr("Localizable", "SETTINGS.AUDIO_SPEED_SECTION.FIXED_TEXTS.TITLE") + } + internal enum MovementSoundInstrument { + /// Movement sound + internal static let title = L10n.tr("Localizable", "SETTINGS.AUDIO_SPEED_SECTION.MOVEMENT_SOUND_INSTRUMENT.TITLE") + } + } internal enum ChangeMovementSoundSheet { /// Choose movement instrument internal static let title = L10n.tr("Localizable", "SETTINGS.CHANGE_MOVEMENT_SOUND_SHEET.TITLE") @@ -201,7 +250,7 @@ internal enum L10n { /// 1. Becoming aware of what you are actually saying when you pray. /// 2. Use the regular prayers to read the Quran. /// - /// Currently the app contains only the last twelve surahs of the Quran and randomly selects between them while one is staying during a prayer, but this is only the first step. With updates we are to follow all the missing surahs, and we also have a planned solution for longer surahs so that you can read them bit by bit to be able to pray and understand them without losing context. + /// Currently the app contains only the last 24 surahs of the Quran and randomly selects between them while one is staying during a prayer, but this is only the first step. With updates we are to follow all the missing surahs, and we also have a solution for longer surahs so that you can read them bit by bit to be able to pray and understand them without losing context. /// /// Specifically NOT the purpose of this app is to teach praying from the ground up. Knowledge of the fundamentals of the prayers is already assumed, the app is intended primarily to move from doing your prayers in a foreign language (Arabic) to a language which you already mastered (currently: English, German and Turkish). internal static let answer = L10n.tr("Localizable", "SETTINGS.FAQ_ENTRIES.APP_MOTIVATION.ANSWER") @@ -211,7 +260,7 @@ internal enum L10n { internal enum IpadReading { /// In the end, this can only be answered by God, but we would like to ask all critics the following question: Are the prayers valid if you do not understand the meaning of your spoken words? internal static let answer = L10n.tr("Localizable", "SETTINGS.FAQ_ENTRIES.IPAD_READING.ANSWER") - /// Are the prayers valid at all, if I read from the iPad? + /// Are the prayers valid at all, if I read or listen to a voice? internal static let question = L10n.tr("Localizable", "SETTINGS.FAQ_ENTRIES.IPAD_READING.QUESTION") } internal enum Language { @@ -222,26 +271,22 @@ internal enum L10n { /// Do prayers not have to be spoken in Arabic, the original language of the Koran? internal static let question = L10n.tr("Localizable", "SETTINGS.FAQ_ENTRIES.LANGUAGE.QUESTION") } - internal enum LanguageMix { - /// Actually this app is meant to be able to get away from the Arabic language, if you did not master it. An alternative usage method would be to use the app as a supplement to prayers in Arabic, in order to be able to read the meaning next to it. Because we are convinced that prayers are all about meaning, we advise against it to not make the prayers unnecessarily complicated, but for some, this may be the only acceptable way what we can understand. We recommend setting the "Changing text name" setting for this application method. - internal static let answer = L10n.tr("Localizable", "SETTINGS.FAQ_ENTRIES.LANGUAGE_MIX.ANSWER") - /// Can I also use the app if I want to continue to pray in Arabic? - internal static let question = L10n.tr("Localizable", "SETTINGS.FAQ_ENTRIES.LANGUAGE_MIX.QUESTION") - } internal enum TranslationProblem { /// We understand that regular prayers are something so important that you want to do everything as correct as possible. We also understand that in religion many different opinions about the meaning of the same text prevail, and it is important that you remain as close as possible to the original text. /// - /// This is why we absolutely want you to read the Quran yourself, not the narratives and traditions of those who have studied the Quran for years and try to enlighten you (supposedly), and above all not those who try to teach you about "the real message of the Koran". We are convinced that this distinction is only possible if you read the Quran for yourself what this App tries to help with. + /// This is why we absolutely want you to read the Quran yourself, not the narratives and traditions of those who have studied the Quran for years and try to enlighten you (supposedly), and above all not those who try to teach you about "the real message of the Koran". We are convinced that one can make distinction only if one reads the Quran for oneself what this App tries to help with. internal static let answer = L10n.tr("Localizable", "SETTINGS.FAQ_ENTRIES.TRANSLATION_PROBLEM.ANSWER") /// Do translations not necessarily change the meaning of the original text? internal static let question = L10n.tr("Localizable", "SETTINGS.FAQ_ENTRIES.TRANSLATION_PROBLEM.QUESTION") } } internal enum FeedbackButton { - /// Send Feedback + /// Feedback internal static let title = L10n.tr("Localizable", "SETTINGS.FEEDBACK_BUTTON.TITLE") } internal enum PrayerSection { + /// You can decide, if the name of the changing Quran recitation while standing should be displayed before showing its contents. You can also allow for up to four times longer Quran recitations while standing. Most Quranic surahs are even too long for that, to include them into your prayers, you have to allow splitting them – the app will remember the spliting position and continue from where you left off on the next prayer. + internal static let footer = L10n.tr("Localizable", "SETTINGS.PRAYER_SECTION.FOOTER") /// Prayer Settings internal static let title = L10n.tr("Localizable", "SETTINGS.PRAYER_SECTION.TITLE") internal enum AllowLongerRecitations { @@ -251,25 +296,13 @@ internal enum L10n { internal static let title = L10n.tr("Localizable", "SETTINGS.PRAYER_SECTION.ALLOW_LONGER_RECITATIONS.TITLE") } internal enum AllowSplittingRecitations { - /// Include long surah by splitting them + /// Split & include lon surah internal static let title = L10n.tr("Localizable", "SETTINGS.PRAYER_SECTION.ALLOW_SPLITTING_RECITATIONS.TITLE") } - internal enum ChangingText { - /// Quran sura speed - internal static let title = L10n.tr("Localizable", "SETTINGS.PRAYER_SECTION.CHANGING_TEXT.TITLE") - } internal enum ChangingTextName { - /// Quran sura name + /// Show Quran sura name internal static let title = L10n.tr("Localizable", "SETTINGS.PRAYER_SECTION.CHANGING_TEXT_NAME.TITLE") } - internal enum FixedTexts { - /// Other texts speed - internal static let title = L10n.tr("Localizable", "SETTINGS.PRAYER_SECTION.FIXED_TEXTS.TITLE") - } - internal enum MovementSoundInstrument { - /// Movement sound - internal static let title = L10n.tr("Localizable", "SETTINGS.PRAYER_SECTION.MOVEMENT_SOUND_INSTRUMENT.TITLE") - } internal enum RakatCount { /// Rakat count internal static let title = L10n.tr("Localizable", "SETTINGS.PRAYER_SECTION.RAKAT_COUNT.TITLE") @@ -280,6 +313,11 @@ internal enum L10n { internal static let title = L10n.tr("Localizable", "SETTINGS.START_BUTTON.TITLE") } } + + internal enum SpeechSynthesizer { + /// Chapter + internal static let bookEmojiReplacement = L10n.tr("Localizable", "SPEECH_SYNTHESIZER.BOOK_EMOJI_REPLACEMENT") + } } // swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length // swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces diff --git a/App/Resources/Colors.xcassets/AccentColor.colorset/Contents.json b/App/Resources/Colors.xcassets/AccentColor.colorset/Contents.json index dc611af..12a5140 100644 --- a/App/Resources/Colors.xcassets/AccentColor.colorset/Contents.json +++ b/App/Resources/Colors.xcassets/AccentColor.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.600", - "green" : "0.522", - "red" : "0.302" + "blue" : "0xCC", + "green" : "0xC2", + "red" : "0xA6" } }, "idiom" : "universal" diff --git a/App/Resources/Colors.xcassets/Secondary.colorset/Contents.json b/App/Resources/Colors.xcassets/Secondary.colorset/Contents.json index 59288dd..3ea17ba 100644 --- a/App/Resources/Colors.xcassets/Secondary.colorset/Contents.json +++ b/App/Resources/Colors.xcassets/Secondary.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.502", - "green" : "0.392", - "red" : "0.090" + "blue" : "0x80", + "green" : "0x63", + "red" : "0x16" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.600", - "green" : "0.522", - "red" : "0.302" + "blue" : "0xCC", + "green" : "0xC2", + "red" : "0xA6" } }, "idiom" : "universal" diff --git a/App/Resources/de.lproj/Localizable.strings b/App/Resources/de.lproj/Localizable.strings index bc5fc12..842027e 100644 --- a/App/Resources/de.lproj/Localizable.strings +++ b/App/Resources/de.lproj/Localizable.strings @@ -1,3 +1,11 @@ +"AUDIO_MODE.MOVEMENT_SOUND" = "Ton bei Positionswechsel"; + +"AUDIO_MODE.MOVEMENT_SOUND_AND_SPEECH_SYNTHESIZER" = "Beides (Ton + Stimme)"; + +"AUDIO_MODE.NONE" = "Kein Audio (stumm)"; + +"AUDIO_MODE.SPEECH_SYNTHESIZER" = "Vorlesende Computer-Stimme"; + "PRAYER_VIEW.COUNTDOWN.NAME" = "Countdown"; "RAKAH_COMPONENT.OPENING_SUPPLICATION.NAME" = "Eröffnungsgebet"; @@ -74,51 +82,65 @@ "RECITATION.THOSE_WHO_DENY_THE_TRUTH.NAME" = "al-Kafirun (Jene, welche die Wahrheit leugnen)"; -"SETTINGS.APP_SECTION.CHANGE_LANGUAGE_BUTTON.TITLE" = "Sprache ändern in der ⚙️ Einstellungen-App"; +"SETTINGS.APP_SECTION.CHANGE_LANGUAGE_BUTTON.TITLE" = "Sprache ändern in ⚙️ Einstellungen"; + +"SETTINGS.APP_SECTION.FOOTER" = "Die Sprache der App bestimmt die Sprache der Texte, die während eines Gebets angezeigt werden. Apple bietet eine app-spezifische Spracheinstellung in der Einstellungen-App. Bitte dort anpassen, um die Sprache dieser App und der Gebete zu ändern."; "SETTINGS.APP_SECTION.TITLE" = "App-Einstellungen"; +"SETTINGS.AUDIO.OUTPUT_DEVICE.TITLE" = "Ausgabegerät festlegen (mit AirPlay-Icon):"; + +"SETTINGS.AUDIO.SPEECH_SYNTHESIZER.PITCH_MULTIPLIER" = "Tonhöhen-Multiplikator"; + +"SETTINGS.AUDIO.SPEECH_SYNTHESIZER.SPEECH_RATE" = "Sprechgeschwindigkeit"; + +"SETTINGS.AUDIO.SPEECH_SYNTHESIZER.VOICE" = "Stimme"; + +"SETTINGS.AUDIO_SPEED_SECTION.AUDIO_MODE.TITLE" = "Audio-Modus"; + +"SETTINGS.AUDIO_SPEED_SECTION.CHANGING_TEXT.TITLE" = "Geschwindigkeit der Koransure"; + +"SETTINGS.AUDIO_SPEED_SECTION.FIXED_TEXTS.TITLE" = "Geschwindigkeit anderer Texte"; + +"SETTINGS.AUDIO_SPEED_SECTION.FOOTER" = "Wähle einen der zwei verfügbaren Audiomodi, oder beide oder keines. Im ersten wird nur ein kurzer Ton abgespielt, wenn während eines Gebets die Position gewechselt wird. Im zweiten Modus wird der gesamte Text laut vorgelesen, sodass man auch nur zuhören kann. Dies funktioniert auch dann, wenn das Gerät gesperrt in der Hosentasche liegt. Dies kann zum Beispiel nützlich sein, wenn die Gebete in der bevorzugten Sprache verrichtet werden sollen, ohne dass andere im selben Gebetsraum dies mitbekommen."; + +"SETTINGS.AUDIO_SPEED_SECTION.MOVEMENT_SOUND_INSTRUMENT.TITLE" = "Bewegungsgeräusch"; + +"SETTINGS.AUDIO_SPEED_SECTION.TITLE" = "Audio- & Geschwindigkeits-Einstellungen"; + "SETTINGS.CHANGE_MOVEMENT_SOUND_SHEET.TITLE" = "Wähle Instrument für Bewegungston"; "SETTINGS.FAQ.TITLE" = "FAQ"; "SETTINGS.FAQ_BUTTON.TITLE" = "FAQ"; -"SETTINGS.FAQ_ENTRIES.APP_MOTIVATION.ANSWER" = "Zwei Ziele werden mit dieser App derzeit verfolgt:\n1. Sich dessen bewusst werden, was man beim Beten eigentlich sagt.\n2. Das regelmäßige Gebet dazu nutzen, den Koran zu lesen.\n\nDerzeit enthält die App zwar nur die letzten zwölf Suren des Koran und wählt zwischen diesen zufällig beim Gebet eines im Stehen aus, jedoch ist dies nur der erste Schritt. Mit Updates sollen alle fehlenden Suren folgen, wobei wir auch eine Lösung für längere Suren vorgesehen haben, sodass man sie Stück für Stück beim Beten lesen kann, ohne den Kontext zu verlieren.\n\nInsbesondere NICHT Ziel dieser App ist es das Beten von Grund auf zu lehren. Wissen über die Grundlagen der Gebete werden bereits vorausgesetzt, die App soll vor allem dazu dienen, vom Beten einer fremden Sprache (Arabisch) in eine Sprache zu wechseln, die man auch beherrscht (derzeit: Deutsch, Englisch und Türkisch)."; +"SETTINGS.FAQ_ENTRIES.APP_MOTIVATION.ANSWER" = "Zwei Ziele werden mit dieser App derzeit verfolgt:\n1. Sich dessen bewusst werden, was man beim Beten eigentlich sagt.\n2. Das regelmäßige Gebet dazu nutzen, den Koran zu lesen.\n\nDerzeit enthält die App zwar nur die letzten 24 Suren des Koran und wählt zwischen diesen zufällig beim Gebet eines im Stehen aus, jedoch ist dies nur der erste Schritt. Mit regelmäßigen Updates sollen alle fehlenden Suren ergänzt werden. Dabei haben wir für längere Suren eine Lösung eingebaut, mit der man sie Stück für Stück beim Beten lesen kann, ohne in einem Gebet zu lange stehen zu müssen.\n\nInsbesondere NICHT Ziel dieser App ist es das Beten von Grund auf zu lehren. Wissen über die Grundlagen der Gebete werden bereits vorausgesetzt, die App soll vor allem dazu dienen, vom Beten einer fremden Sprache (Arabisch) in eine Sprache zu wechseln, die man auch beherrscht (derzeit: Deutsch, Englisch und Türkisch)."; "SETTINGS.FAQ_ENTRIES.APP_MOTIVATION.QUESTION" = "Was ist der Sinn dieser App?"; -"SETTINGS.FAQ_ENTRIES.IPAD_READING.ANSWER" = "Das kann letzten Endes nur Gott allein beantworten, wir möchten aber allen Kritikern folgende Gegenfrage stellen: Sind die Gebete denn gültig, wenn ihr selbst die Bedeutung eurer gesprochenen Worte nicht versteht?"; +"SETTINGS.FAQ_ENTRIES.IPAD_READING.ANSWER" = "Das kann letzten Endes nur Gott allein beantworten, wir möchten aber allen Kritikern folgende Gegenfrage stellen: Sind die Gebete denn gültig, wenn ihr selbst die Bedeutung eurer auf Arabisch gesprochenen Worte nicht versteht?"; -"SETTINGS.FAQ_ENTRIES.IPAD_READING.QUESTION" = "Sind die Gebete denn überhaupt gültig, wenn ich während dessen vom iPad ablese?"; +"SETTINGS.FAQ_ENTRIES.IPAD_READING.QUESTION" = "Sind die Gebete denn überhaupt gültig, wenn ich während dessen ablese oder einer Stimme zuhöre?"; "SETTINGS.FAQ_ENTRIES.LANGUAGE.ANSWER" = "Wir sind der Überzeugung, dass der Koran das Gebet vor allem dazu vorschreibt, sich Gottes stets bewusst zu sein und sich seiner Rechtleitung zu erinnern. Worte, die man nicht versteht können weder vor dem falschen Weg warnen, noch den richtigen Weg aufzeigen, was ja die Ziele des Koran sind.\n\nWir können uns jedenfalls nicht vorstellen, dass Gott von uns nur möchte, dass wir fünf mal täglich die Schönheit des Klangs seiner Lyrik bewundern. Das Arabisch im Koran mag perfekt sein und in seiner Bedeutung vollkommen, doch solange der Betende des Arabischen nicht mächtig ist, ist sein Gebet auf Arabisch unvollkommen. Zudem kennen wir keinen Koranvers, der das Beten in arabischer Sprache vorschreibt."; "SETTINGS.FAQ_ENTRIES.LANGUAGE.QUESTION" = "Müssen Gebete nicht auf Arabisch gesprochen werden, der Originalsprache des Koran?"; -"SETTINGS.FAQ_ENTRIES.LANGUAGE_MIX.ANSWER" = "Eigentlich ist diese App dazu gedacht, gerade von der arabischen Sprache weg kommen zu können, wenn man sie nicht beherrscht. Eine alternative Nutzungsmethode wäre es, die App nur ergänzend zum Gebet auf Arabisch einzusetzen, um die Bedeutung nebenher lesen zu können. Weil wir davon überzeugt sind, dass es beim Beten allein um die Bedeutung geht, raten wir zwar hiervon ab um das Gebet nicht unnötig zu verkomplizieren, jedoch mag dies für manche der einzige für sie akzeptable Weg sein, wofür wir Verständnis haben. Wir empfehlen für diese Anwendungsmethode die Einstellung „Name für wechselnden Text“ an zu lassen."; - -"SETTINGS.FAQ_ENTRIES.LANGUAGE_MIX.QUESTION" = "Kann ich die App auch nutzen, wenn ich weiter auf Arabisch beten will?"; - -"SETTINGS.FAQ_ENTRIES.TRANSLATION_PROBLEM.ANSWER" = "Wir verstehen, dass das Gebet etwas so wichtiges ist, dass man alles möglichst richtig machen möchte. Wir verstehen auch, dass gerade in der Religion viele unterschiedliche Auffassungen über die Bedeutung desselben Textes vorherrschen und gerade dabei wichtig ist, dass man so nah wie möglich am Original bleiben muss.\n\nDeshalb wollen wir ja auch unbedingt, dass ihr selbst den Koran lest, und zwar den Koran selbst, nicht die Überlieferungen und Erzählungen derer, die den Koran Jahre lang studiert haben und euch (vermeintlich) aufklären wollen, und vor allem nicht jener, die euch „die eigentliche Botschaft des Koran“ beibringen wollen. Wir sind davon überzeugt, dass diese Unterscheidung nur dann geht, wenn ihr selbst den Koran lest, wobei euch diese App helfen soll."; +"SETTINGS.FAQ_ENTRIES.TRANSLATION_PROBLEM.ANSWER" = "Wir verstehen, dass das Gebet etwas so wichtiges ist, dass man alles möglichst richtig machen möchte. Wir verstehen auch, dass gerade in der Religion viele unterschiedliche Auffassungen über die Bedeutung desselben Textes vorherrschen und gerade dabei wichtig ist, dass man so nah wie möglich am Original bleiben muss.\n\nDeshalb wollen wir ja auch unbedingt, dass ihr selbst den Koran lest, und zwar den Koran selbst, nicht die Überlieferungen und Erzählungen derer, die den Koran Jahre lang studiert haben und euch (vermeintlich) aufklären wollen, und vor allem nicht jener, die euch „die eigentliche Botschaft des Koran“ beibringen wollen. Wir sind davon überzeugt, dass man diese Unterscheidung nur dann wirklich vornehmen kann, wenn man selbst den Koran liest, wobei diese App helfen soll."; "SETTINGS.FAQ_ENTRIES.TRANSLATION_PROBLEM.QUESTION" = "Ändern Übersetzungen nicht zwangsweise die Bedeutung des Originaltextes?"; -"SETTINGS.FEEDBACK_BUTTON.TITLE" = "Feedback senden"; +"SETTINGS.FEEDBACK_BUTTON.TITLE" = "Feedback"; "SETTINGS.PRAYER_SECTION.ALLOW_LONGER_RECITATIONS.RESET_MESSAGE" = "Aktuelle Position in geteilter Sure wurde gelöscht."; "SETTINGS.PRAYER_SECTION.ALLOW_LONGER_RECITATIONS.TITLE" = "Erlaube längere Rezitationen"; -"SETTINGS.PRAYER_SECTION.ALLOW_SPLITTING_RECITATIONS.TITLE" = "Inkludiere lange Suren durch Aufteilung"; - -"SETTINGS.PRAYER_SECTION.CHANGING_TEXT.TITLE" = "Geschwindigkeit der Koransure"; +"SETTINGS.PRAYER_SECTION.ALLOW_SPLITTING_RECITATIONS.TITLE" = "Lange Suren aufteilen & inkludieren"; -"SETTINGS.PRAYER_SECTION.CHANGING_TEXT_NAME.TITLE" = "Name der Koransure"; +"SETTINGS.PRAYER_SECTION.CHANGING_TEXT_NAME.TITLE" = "Name der Koransure einblenden"; -"SETTINGS.PRAYER_SECTION.FIXED_TEXTS.TITLE" = "Geschwindigkeit anderer Texte"; - -"SETTINGS.PRAYER_SECTION.MOVEMENT_SOUND_INSTRUMENT.TITLE" = "Bewegungsgeräusch"; +"SETTINGS.PRAYER_SECTION.FOOTER" = "Wähle, ob der Name der sich ändernden Koranrezitation im Stehen angezeigt werden soll, bevor ihr Inhalt angezeigt wird. Es können auch bis zu viermal längere Koranrezitationen im Stehen zugelassen werden. Die meisten Koransuren sind selbst dafür zu lang, um sie in die Gebete einzubeziehen, muss das Teilen von Suren erlaubt werden – die App merkt sich die Stelle dann und macht beim nächsten Gebet dort weiter, wo zuletzt aufgehört."; "SETTINGS.PRAYER_SECTION.RAKAT_COUNT.TITLE" = "Anzahl der Rakat"; @@ -127,3 +149,5 @@ "SETTINGS.START_BUTTON.TITLE" = "Gebet starten"; "SETTINGS.TITLE" = "Einstellungen"; + +"SPEECH_SYNTHESIZER.BOOK_EMOJI_REPLACEMENT" = "Kapitel "; diff --git a/App/Resources/en.lproj/Localizable.strings b/App/Resources/en.lproj/Localizable.strings index 9e2f7ff..cefe9aa 100644 --- a/App/Resources/en.lproj/Localizable.strings +++ b/App/Resources/en.lproj/Localizable.strings @@ -1,3 +1,11 @@ +"AUDIO_MODE.MOVEMENT_SOUND" = "Sound for position changes"; + +"AUDIO_MODE.MOVEMENT_SOUND_AND_SPEECH_SYNTHESIZER" = "Both (sound + voice)"; + +"AUDIO_MODE.NONE" = "No audio (muted)"; + +"AUDIO_MODE.SPEECH_SYNTHESIZER" = "Reading computer voice"; + "PRAYER_VIEW.COUNTDOWN.NAME" = "Countdown"; "RAKAH_COMPONENT.OPENING_SUPPLICATION.NAME" = "Opening Supplication"; @@ -74,51 +82,65 @@ "RECITATION.THOSE_WHO_DENY_THE_TRUTH.NAME" = "al-Kafirun (Those Who Deny The Truth)"; -"SETTINGS.APP_SECTION.CHANGE_LANGUAGE_BUTTON.TITLE" = "Change app language in ⚙️ Settings app"; +"SETTINGS.APP_SECTION.CHANGE_LANGUAGE_BUTTON.TITLE" = "Change language in ⚙️ Settings"; + +"SETTINGS.APP_SECTION.FOOTER" = "The language of the app determines the language of the texts shown during a prayer. Apple provides an app-specific language setting in the Settings app. Please use it to change the language of this app and the prayers."; "SETTINGS.APP_SECTION.TITLE" = "App Settings"; +"SETTINGS.AUDIO.OUTPUT_DEVICE.TITLE" = "Select output device (via AirPlay icon):"; + +"SETTINGS.AUDIO.SPEECH_SYNTHESIZER.PITCH_MULTIPLIER" = "Pitch multiplicator"; + +"SETTINGS.AUDIO.SPEECH_SYNTHESIZER.SPEECH_RATE" = "Speech rate"; + +"SETTINGS.AUDIO.SPEECH_SYNTHESIZER.VOICE" = "Voice"; + +"SETTINGS.AUDIO_SPEED_SECTION.AUDIO_MODE.TITLE" = "Audio Mode"; + +"SETTINGS.AUDIO_SPEED_SECTION.CHANGING_TEXT.TITLE" = "Quran sura speed"; + +"SETTINGS.AUDIO_SPEED_SECTION.FIXED_TEXTS.TITLE" = "Other texts speed"; + +"SETTINGS.AUDIO_SPEED_SECTION.FOOTER" = "Choose one of the two available audio modes, or both or none. The first only plays a short sound when changing positions during a prayer. The second will instead read out loud the full prayer text so you can just listen. This also works when the device is locked in your pocket. This can for example be useful when you want to do your prayers in your preferred language without others in the same Prayer room noticing it."; + +"SETTINGS.AUDIO_SPEED_SECTION.MOVEMENT_SOUND_INSTRUMENT.TITLE" = "Movement sound"; + +"SETTINGS.AUDIO_SPEED_SECTION.TITLE" = "Audio & Speed Settings"; + "SETTINGS.CHANGE_MOVEMENT_SOUND_SHEET.TITLE" = "Choose movement instrument"; "SETTINGS.FAQ.TITLE" = "FAQ"; "SETTINGS.FAQ_BUTTON.TITLE" = "FAQ"; -"SETTINGS.FAQ_ENTRIES.APP_MOTIVATION.ANSWER" = "This App currently has two goals:\n1. Becoming aware of what you are actually saying when you pray.\n2. Use the regular prayers to read the Quran.\n\nCurrently the app contains only the last twelve surahs of the Quran and randomly selects between them while one is staying during a prayer, but this is only the first step. With updates we are to follow all the missing surahs, and we also have a planned solution for longer surahs so that you can read them bit by bit to be able to pray and understand them without losing context.\n\nSpecifically NOT the purpose of this app is to teach praying from the ground up. Knowledge of the fundamentals of the prayers is already assumed, the app is intended primarily to move from doing your prayers in a foreign language (Arabic) to a language which you already mastered (currently: English, German and Turkish)."; +"SETTINGS.FAQ_ENTRIES.APP_MOTIVATION.ANSWER" = "This App currently has two goals:\n1. Becoming aware of what you are actually saying when you pray.\n2. Use the regular prayers to read the Quran.\n\nCurrently the app contains only the last 24 surahs of the Quran and randomly selects between them while one is staying during a prayer, but this is only the first step. With updates we are to follow all the missing surahs, and we also have a solution for longer surahs so that you can read them bit by bit to be able to pray and understand them without losing context.\n\nSpecifically NOT the purpose of this app is to teach praying from the ground up. Knowledge of the fundamentals of the prayers is already assumed, the app is intended primarily to move from doing your prayers in a foreign language (Arabic) to a language which you already mastered (currently: English, German and Turkish)."; "SETTINGS.FAQ_ENTRIES.APP_MOTIVATION.QUESTION" = "What is the purpose of this app?"; "SETTINGS.FAQ_ENTRIES.IPAD_READING.ANSWER" = "In the end, this can only be answered by God, but we would like to ask all critics the following question: Are the prayers valid if you do not understand the meaning of your spoken words?"; -"SETTINGS.FAQ_ENTRIES.IPAD_READING.QUESTION" = "Are the prayers valid at all, if I read from the iPad?"; +"SETTINGS.FAQ_ENTRIES.IPAD_READING.QUESTION" = "Are the prayers valid at all, if I read or listen to a voice?"; "SETTINGS.FAQ_ENTRIES.LANGUAGE.ANSWER" = "We are convinced that the Quran prescribes regular prayers above all to be aware of God and to remember his guidance. Words that can not be understood neither warn against the wrong path, nor show the right path which is, at the end, what the Quran aims to do.\n\nIn any case, we can not imagine that God wants us to merely admire the beauty of the sound of his lyrics five times a day. The Arabic in the Quran may be perfect and supreme in its meaning, but as long as the person doing the prayer has not mastered the Arabic language, his prayers in Arabic are imperfect. Moreover, we do not know a Quran verse which requires prayers to be done in Arabic."; "SETTINGS.FAQ_ENTRIES.LANGUAGE.QUESTION" = "Do prayers not have to be spoken in Arabic, the original language of the Koran?"; -"SETTINGS.FAQ_ENTRIES.LANGUAGE_MIX.ANSWER" = "Actually this app is meant to be able to get away from the Arabic language, if you did not master it. An alternative usage method would be to use the app as a supplement to prayers in Arabic, in order to be able to read the meaning next to it. Because we are convinced that prayers are all about meaning, we advise against it to not make the prayers unnecessarily complicated, but for some, this may be the only acceptable way what we can understand. We recommend setting the \"Changing text name\" setting for this application method."; - -"SETTINGS.FAQ_ENTRIES.LANGUAGE_MIX.QUESTION" = "Can I also use the app if I want to continue to pray in Arabic?"; - -"SETTINGS.FAQ_ENTRIES.TRANSLATION_PROBLEM.ANSWER" = "We understand that regular prayers are something so important that you want to do everything as correct as possible. We also understand that in religion many different opinions about the meaning of the same text prevail, and it is important that you remain as close as possible to the original text.\n\nThis is why we absolutely want you to read the Quran yourself, not the narratives and traditions of those who have studied the Quran for years and try to enlighten you (supposedly), and above all not those who try to teach you about \"the real message of the Koran\". We are convinced that this distinction is only possible if you read the Quran for yourself what this App tries to help with."; +"SETTINGS.FAQ_ENTRIES.TRANSLATION_PROBLEM.ANSWER" = "We understand that regular prayers are something so important that you want to do everything as correct as possible. We also understand that in religion many different opinions about the meaning of the same text prevail, and it is important that you remain as close as possible to the original text.\n\nThis is why we absolutely want you to read the Quran yourself, not the narratives and traditions of those who have studied the Quran for years and try to enlighten you (supposedly), and above all not those who try to teach you about \"the real message of the Koran\". We are convinced that one can make distinction only if one reads the Quran for oneself what this App tries to help with."; "SETTINGS.FAQ_ENTRIES.TRANSLATION_PROBLEM.QUESTION" = "Do translations not necessarily change the meaning of the original text?"; -"SETTINGS.FEEDBACK_BUTTON.TITLE" = "Send Feedback"; +"SETTINGS.FEEDBACK_BUTTON.TITLE" = "Feedback"; "SETTINGS.PRAYER_SECTION.ALLOW_LONGER_RECITATIONS.RESET_MESSAGE" = "Current position in split Surah was cleared."; "SETTINGS.PRAYER_SECTION.ALLOW_LONGER_RECITATIONS.TITLE" = "Allow longer recitations"; -"SETTINGS.PRAYER_SECTION.ALLOW_SPLITTING_RECITATIONS.TITLE" = "Include long surah by splitting them"; - -"SETTINGS.PRAYER_SECTION.CHANGING_TEXT.TITLE" = "Quran sura speed"; +"SETTINGS.PRAYER_SECTION.ALLOW_SPLITTING_RECITATIONS.TITLE" = "Split & include lon surah"; -"SETTINGS.PRAYER_SECTION.CHANGING_TEXT_NAME.TITLE" = "Quran sura name"; +"SETTINGS.PRAYER_SECTION.CHANGING_TEXT_NAME.TITLE" = "Show Quran sura name"; -"SETTINGS.PRAYER_SECTION.FIXED_TEXTS.TITLE" = "Other texts speed"; - -"SETTINGS.PRAYER_SECTION.MOVEMENT_SOUND_INSTRUMENT.TITLE" = "Movement sound"; +"SETTINGS.PRAYER_SECTION.FOOTER" = "You can decide, if the name of the changing Quran recitation while standing should be displayed before showing its contents. You can also allow for up to four times longer Quran recitations while standing. Most Quranic surahs are even too long for that, to include them into your prayers, you have to allow splitting them – the app will remember the spliting position and continue from where you left off on the next prayer."; "SETTINGS.PRAYER_SECTION.RAKAT_COUNT.TITLE" = "Rakat count"; @@ -127,3 +149,5 @@ "SETTINGS.START_BUTTON.TITLE" = "Start prayer"; "SETTINGS.TITLE" = "Settings"; + +"SPEECH_SYNTHESIZER.BOOK_EMOJI_REPLACEMENT" = "Chapter "; diff --git a/App/Resources/tr.lproj/Localizable.strings b/App/Resources/tr.lproj/Localizable.strings index 7992b58..758880e 100644 --- a/App/Resources/tr.lproj/Localizable.strings +++ b/App/Resources/tr.lproj/Localizable.strings @@ -1,3 +1,11 @@ +"AUDIO_MODE.MOVEMENT_SOUND" = "Konum değişiklik tonu"; + +"AUDIO_MODE.MOVEMENT_SOUND_AND_SPEECH_SYNTHESIZER" = "Her ikisi de (ton + okuma)"; + +"AUDIO_MODE.NONE" = "Sessiz"; + +"AUDIO_MODE.SPEECH_SYNTHESIZER" = "Bilgisayar sesli okuma"; + "PRAYER_VIEW.COUNTDOWN.NAME" = "Geri Sayım"; "RAKAH_COMPONENT.OPENING_SUPPLICATION.NAME" = "Açılış Duası"; @@ -74,37 +82,55 @@ "RECITATION.THOSE_WHO_DENY_THE_TRUTH.NAME" = "Kafirun (İnkarcılar)"; -"SETTINGS.APP_SECTION.CHANGE_LANGUAGE_BUTTON.TITLE" = "⚙️ Ayarlar uygulamasında dili değiştir"; +"SETTINGS.APP_SECTION.CHANGE_LANGUAGE_BUTTON.TITLE" = "⚙️ Ayarlar'da dili değiştir"; + +"SETTINGS.APP_SECTION.FOOTER" = "Uygulamanın dili, bir dua sırasında gösterilen metinlerin dilini belirler. Apple, Ayarlar uygulamasında uygulamaya özel bir dil ayarı sağlar. Lütfen bu uygulamanın dilini ve duaları değiştirmek için o ayarı kullanın."; "SETTINGS.APP_SECTION.TITLE" = "Uygulama ayarları"; +"SETTINGS.AUDIO.OUTPUT_DEVICE.TITLE" = "Çıkış cihazını seçin (AirPlay simgesi ile):"; + +"SETTINGS.AUDIO.SPEECH_SYNTHESIZER.PITCH_MULTIPLIER" = "Pitch Çarpanı"; + +"SETTINGS.AUDIO.SPEECH_SYNTHESIZER.SPEECH_RATE" = "Konuşma hızı"; + +"SETTINGS.AUDIO.SPEECH_SYNTHESIZER.VOICE" = "Ses"; + +"SETTINGS.AUDIO_SPEED_SECTION.AUDIO_MODE.TITLE" = "Ses Modu"; + +"SETTINGS.AUDIO_SPEED_SECTION.CHANGING_TEXT.TITLE" = "Kur'an âyet hızı"; + +"SETTINGS.AUDIO_SPEED_SECTION.FIXED_TEXTS.TITLE" = "Diğer metin hızı"; + +"SETTINGS.AUDIO_SPEED_SECTION.FOOTER" = "Mevcut iki ses modundan birini seçin, yada ikisinide yada hiç birini. İlkinde, bir dua sırasında pozisyon değiştirildiğinde sadece kısa bir ses çalınır. İkinci modda, tüm metin yüksek sesle okunur, böylece yalnızca dinleyerek kılınabilir. Bu, cihaz cebinizde kilitliyken de çalışır. Bu mesela aynı mescitteki diğer kişiler farkında olmadan tercih edilen dilde kılmak için faydalı olabilir."; + +"SETTINGS.AUDIO_SPEED_SECTION.MOVEMENT_SOUND_INSTRUMENT.TITLE" = "Hareket tonu"; + +"SETTINGS.AUDIO_SPEED_SECTION.TITLE" = "Ses ve hız ayarları"; + "SETTINGS.CHANGE_MOVEMENT_SOUND_SHEET.TITLE" = "Hareket tonun enstrumanını seç"; "SETTINGS.FAQ.TITLE" = "SSS"; "SETTINGS.FAQ_BUTTON.TITLE" = "SSS"; -"SETTINGS.FAQ_ENTRIES.APP_MOTIVATION.ANSWER" = "Bu uygulamanın şu anda iki hedefi vardır:\n1. Namaz kılarken ne söylenildiğinin farkında olunmasi.\n2. Namaz kılaraken Kuranın hatim edilmesi.\n\nŞu anda uygulama sadece Kurandaki son oniki sûreyi içerir ve namazda kıyamda iken rastgele bir sure seçer. Ama bu sadece ilk adımdır. Tüm eksik olan sûreler güncelleştirmeler ile tamamen ilave edilecektir ki, namaz kılarken parça parça Kuranı okuyabilesiniz. Uzun sureler için de bir çözümümüz var.\n\nBu uygulamanın amacı namaz kılmayı sıfırdan öğretmek DEĞİLDİR. Bu uygulama namazdaki sureleri yabanci dil (Arapça) yerine, bildiğiniz bir dilden okuyabilmeniz için hazırlanmıştır. Şu anda dil olarak Türkçe, Almanca ve İngilizce mevcuttur."; +"SETTINGS.FAQ_ENTRIES.APP_MOTIVATION.ANSWER" = "Bu uygulamanın şu anda iki hedefi vardır:\n1. Namaz kılarken ne söylenildiğinin farkında olunmasi.\n2. Namaz kılarken Kuranın hatim edilmesi.\n\nŞu anda uygulama sadece Kurandaki son 24 sûreyi içerir ve namazda kıyamda iken rastgele bir sure seçer. Ama bu sadece ilk adımdır. Tüm eksik olan sûreler güncelleştirmeler ile tamamen ilave edilecektir ki, namaz kılarken parça parça Kuranı okuyabilesiniz. Uzun sureler için de parça parça okuma çözümümüz var.\n\nBu uygulamanın amacı namaz kılmayı sıfırdan öğretmek DEĞİLDİR. Bu uygulama namazdaki sureleri yabanci dil (Arapça) yerine, bildiğiniz bir dilden okuyabilmeniz için hazırlanmıştır. Şu anda dil olarak Türkçe, Almanca ve İngilizce mevcuttur."; "SETTINGS.FAQ_ENTRIES.APP_MOTIVATION.QUESTION" = "Bu uygulamanın amacı nedir?"; "SETTINGS.FAQ_ENTRIES.IPAD_READING.ANSWER" = "Bu soruya sonuçta sadece Tanrımız cevap verebilir, ama biz tüm eleştirmenlere şu soruyu sormak istiyoruz: Eğer namazda konuşulan kelimelerin anlamı bilinmez ise, namazın gerçekten kabul olabileceğini mi düşünüyorsunuz?"; -"SETTINGS.FAQ_ENTRIES.IPAD_READING.QUESTION" = "Namazda kelimeler iPad den okunulursa, namaz hiç geçerli olabilirmi?"; +"SETTINGS.FAQ_ENTRIES.IPAD_READING.QUESTION" = "Namazda kelimeler cihaz dan okunulursa yada sesli dinlenilirse, namaz hiç geçerli olabilirmi?"; "SETTINGS.FAQ_ENTRIES.LANGUAGE.ANSWER" = "Kuranı bizim içinde yazan şeylerden haberdar olmamız ve hayatımıza aktarmamız için indiğini düşünüyoruz. Anlamadığımız kelimeler ne bize yanlış yolu gösterir, nede bizi doğru yola iletir. Fakat bu Kuranın en büyük hedefidir.\n\nAllah günde beş vakit onun kelimelerinin anlamsız bir şekilde kulağa güzel gelen bir siir gibi dinlememizi istediğine inanamıyoruz. Kurandaki Arapça Allahın bize gönderdiği en doğru mesaj olmasına rağmen, Arapça diline hakim olmayan bir kişinin arapça okunan namazın kabul edildiğine inanamıyoruz. Bu tür bir namazda büyük bir eksiklik vardır. Üstelik bizim bilgimize göre Kuranda namazın Arapça dilde okunması meçbur olduğunu söğleğen bir ayet bulunmamaktadır."; "SETTINGS.FAQ_ENTRIES.LANGUAGE.QUESTION" = "Namaz daki sureler ve dualar Kuranın asıl dili Arapçadan okunması şart değilmidir?"; -"SETTINGS.FAQ_ENTRIES.LANGUAGE_MIX.ANSWER" = "Aslında bu uygulamanın hedefi Arapçaya hakim olmadığınız sürece namazda kullanmamak. Kullanım alternatifi olarak namazı hala Arapça dilinde devam etmek isterseniz, bu uygulamayı ilaveten okuyarak da kullanmanız mümkündür. Bizce bu namazınızı boş yere zorlaştıracaktır, bu yüzden tavsiyemiz duaları veya sureleri bildiğiniz dilden okumak. Yinede yüzde yüz emin olamadıysanız Arapçadan okuma isteğinizi anlıyabiliriz. O zaman \"Değişen metin adı\" ayarını kullanınız."; - -"SETTINGS.FAQ_ENTRIES.LANGUAGE_MIX.QUESTION" = "Ben Arapça dua etmeye devam etmek istersem, uygulamayı kullanabilir miyim?"; - "SETTINGS.FAQ_ENTRIES.TRANSLATION_PROBLEM.ANSWER" = "Biz namaz kılmanın çok önemli bir ibadet olduğu için, mümkün olduğu kadar doğru yapmak istediğinizi anlıyoruz. Bizde bu sebeple ayrıca dinde aynı metnin anlamları hakkında çok farklı görüşlerin olduğunu bildiğimiz için, sizler icin orijinala yakın bir meal kullanmayı tercih ettik.\n\nBu nedenle, biz de Kuranın kendisini okumanızın şart olduğunu düşünerek bu uygulamayı size bir yardım olarak hazırladık. Kuranın içindekilerine başkaların (sözde) hikayelerini ve hurafelerini dinleyerek onların size \"Kuranın gerçek mesajını\" öğretmesine engel olabilmeniz için Kuranın ta kendisini okumayı öneriyoruz. Onların dediklerinin doğru olup olmadığını sadece böylece ayrım edebileceğinize inanıyoruz."; "SETTINGS.FAQ_ENTRIES.TRANSLATION_PROBLEM.QUESTION" = "Çeviriler otomatikmen orijinal metnin anlamını değiştirmezmi?"; -"SETTINGS.FEEDBACK_BUTTON.TITLE" = "Geribildirim yolla"; +"SETTINGS.FEEDBACK_BUTTON.TITLE" = "Geri bildirim"; "SETTINGS.PRAYER_SECTION.ALLOW_LONGER_RECITATIONS.RESET_MESSAGE" = "Bölünmüş suredeki şu anki pozisyon silindi."; @@ -112,13 +138,9 @@ "SETTINGS.PRAYER_SECTION.ALLOW_SPLITTING_RECITATIONS.TITLE" = "Uzun surelere parçalayarak izin ver"; -"SETTINGS.PRAYER_SECTION.CHANGING_TEXT.TITLE" = "Kur'an âyet hızı"; - -"SETTINGS.PRAYER_SECTION.CHANGING_TEXT_NAME.TITLE" = "Kur'an âyet adı"; +"SETTINGS.PRAYER_SECTION.CHANGING_TEXT_NAME.TITLE" = "Kur'an âyet adı göster"; -"SETTINGS.PRAYER_SECTION.FIXED_TEXTS.TITLE" = "Diğer metin hızı"; - -"SETTINGS.PRAYER_SECTION.MOVEMENT_SOUND_INSTRUMENT.TITLE" = "Hareket tonu"; +"SETTINGS.PRAYER_SECTION.FOOTER" = "İçeriğini göstermeden önce ayakta dururken değişen Kuran kıraatinin adının görüntülenip görüntülenmeyeceğini seçin. Ayaktayken dört kata kadar daha uzun Kuran okumalarına izin vermek de mümkündür. Kuran surelerinin çoğu bunun için bile fazladan uzundur, bu sureleri de namaza eklemek için paylaşılmasına izin verilmelidir - uygulama daha sonra pasajı hatırlar ve bir sonraki dua ile kaldığı yerden devam etmesini sağlar."; "SETTINGS.PRAYER_SECTION.RAKAT_COUNT.TITLE" = "Rekat sayısı"; @@ -127,3 +149,5 @@ "SETTINGS.START_BUTTON.TITLE" = "Namazı başlat"; "SETTINGS.TITLE" = "Ayarlar"; + +"SPEECH_SYNTHESIZER.BOOK_EMOJI_REPLACEMENT" = "Sure "; diff --git a/App/Sources/AppDelegate.swift b/App/Sources/AppDelegate.swift index 1dd7e8e..01ab2bf 100644 --- a/App/Sources/AppDelegate.swift +++ b/App/Sources/AppDelegate.swift @@ -3,12 +3,12 @@ // Copyright © 2017 Flinesoft. All rights reserved. // +import AVKit import Imperio import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - // MARK: - Stored Instance Properties var window: UIWindow? var initialFlowCtrl: InitialFlowController? @@ -19,6 +19,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window = UIWindow(frame: UIScreen.main.bounds) window?.makeKeyAndVisible() + // reset user defaults for UI Tests + if ProcessInfo.processInfo.arguments.contains("UI_TESTS") { + UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) + } + // setup global stuff Logger.shared.setup() ErrorHandler.shared.setup(window: window!) diff --git a/App/Sources/Globals/AudioPlayer.swift b/App/Sources/Globals/AudioPlayer.swift index 852e277..41403c8 100644 --- a/App/Sources/Globals/AudioPlayer.swift +++ b/App/Sources/Globals/AudioPlayer.swift @@ -6,13 +6,19 @@ import AVFoundation final class AudioPlayer { - // MARK: - Stored Type Properties static let shared = AudioPlayer() - // MARK: - Instance Properties private var audioPlayer: AVAudioPlayer? - // MARK: - Instance Methods + private init() { + try? AVAudioSession.sharedInstance() + .setCategory( + .playback, + mode: .voicePrompt, + options: [.mixWithOthers, .allowAirPlay, .allowBluetooth, .allowBluetoothA2DP] + ) + } + func movementSoundUrl(name: String, instrument: String) -> URL? { Bundle.main.url(forResource: instrument, withExtension: "caf", subdirectory: name) } diff --git a/App/Sources/Globals/Countdown.swift b/App/Sources/Globals/Countdown.swift index 63a18cd..3d1292a 100644 --- a/App/Sources/Globals/Countdown.swift +++ b/App/Sources/Globals/Countdown.swift @@ -7,21 +7,18 @@ import SwiftyTimer import UIKit class Countdown { - // MARK: - Stored Instance Properties private var currentValue: Int private var timer: Timer? private var countClosure: ((_ count: Int) -> Void)? private var finishClosure: (() -> Void)? - // MARK: - Initializers init( startValue: Int ) { self.currentValue = startValue } - // MARK: - Instance Methods func start() { timer = Timer.after(1) { self.currentValue -= 1 diff --git a/App/Sources/Globals/Logger.swift b/App/Sources/Globals/Logger.swift index e0d20e8..becf7ee 100644 --- a/App/Sources/Globals/Logger.swift +++ b/App/Sources/Globals/Logger.swift @@ -6,15 +6,11 @@ import SwiftyBeaver import UIKit -// MARK: - Global Objects let log = SwiftyBeaver.self -// MARK: - Helper Class final class Logger { - // MARK: - Stored Type Properties static let shared = Logger() - // MARK: - Instance Properties func setup() { // log to console let consoleDestination = ConsoleDestination() diff --git a/App/Sources/Globals/SpeechSynthesizer.swift b/App/Sources/Globals/SpeechSynthesizer.swift new file mode 100644 index 0000000..03bbd3a --- /dev/null +++ b/App/Sources/Globals/SpeechSynthesizer.swift @@ -0,0 +1,111 @@ +// +// Created by Cihat Gündüz on 18.10.21. +// Copyright © 2021 Flinesoft. All rights reserved. +// + +import Foundation +import AVFoundation +import HandySwift + +final class SpeechSynthesizer: NSObject { + enum SupportedLanguage: String { + case english = "en" + case german = "de" + case turkish = "tr" + + static var current: SupportedLanguage { + .init(rawValue: Locale.current.languageCode!)! + } + + static var bestMatchingVoice: AVSpeechSynthesisVoice { + guard let regionCode = Locale.current.regionCode else { return current.voices[0] } + guard let voice = AVSpeechSynthesisVoice.speechVoices().first(where: { $0.language.hasSuffix(regionCode) }) else { + return current.voices[0] + } + + return voice + } + + var voices: [AVSpeechSynthesisVoice] { + AVSpeechSynthesisVoice.speechVoices().filter { $0.language.hasPrefix(rawValue) } + } + } + + let voice: AVSpeechSynthesisVoice + let pitchMultiplier: Float + let speechRate: Float + + let synthesizer: AVSpeechSynthesizer = .init() + + fileprivate var completionBlock: (() -> Void)? + fileprivate var completionDelay: TimeInterval? + + init( + voice: AVSpeechSynthesisVoice, + pitchMultiplier: Float, + speechRate: Float + ) { + self.voice = voice + self.pitchMultiplier = pitchMultiplier + self.speechRate = speechRate + + try? AVAudioSession.sharedInstance() + .setCategory( + .playback, + mode: .voicePrompt, + options: [.mixWithOthers, .allowAirPlay, .allowBluetooth, .allowBluetoothA2DP] + ) + + super.init() + synthesizer.delegate = self + } + + func speak( + text: String, + completion: (() -> Void)? = nil, + delayCompletion: TimeInterval? = nil + ) { + let textToSpeak: String = { + if text.contains("📖") { + return text.replacingOccurrences(of: "📖", with: L10n.SpeechSynthesizer.bookEmojiReplacement) + } + else { + // remove braces & brackets, e.g.: "storming (blindly) into [any] host" -> "storming blindly into any host" + let bracesRegexes = [try! Regex(#"\((.*)\)"#), try! Regex(#"\[(.*)\]"#)] + return bracesRegexes.reduce(text) { partialResult, bracesRegex in + bracesRegex.replacingMatches(in: partialResult, with: "$1") + } + } + }() + + let utterance = AVSpeechUtterance(string: textToSpeak) + utterance.voice = voice + utterance.pitchMultiplier = pitchMultiplier + utterance.volume = 1.0 + utterance.rate = speechRate + utterance.preUtteranceDelay = .zero + utterance.postUtteranceDelay = .zero + + self.completionBlock = completion + self.completionDelay = delayCompletion + + synthesizer.speak(utterance) + } + + func stop() { + self.completionBlock = nil + synthesizer.stopSpeaking(at: .immediate) + } +} + +extension SpeechSynthesizer: AVSpeechSynthesizerDelegate { + func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { + if let completionBlock = completionBlock { + delay(by: completionDelay ?? .zero) { + self.completionBlock = nil + self.completionDelay = nil + completionBlock() + } + } + } +} diff --git a/App/Sources/Models/AudioMode.swift b/App/Sources/Models/AudioMode.swift new file mode 100644 index 0000000..3fa6771 --- /dev/null +++ b/App/Sources/Models/AudioMode.swift @@ -0,0 +1,29 @@ +// +// Created by Cihat Gündüz on 23.10.21. +// Copyright © 2021 Flinesoft. All rights reserved. +// + +import Foundation + +enum AudioMode: String, CaseIterable, Equatable { + case movementSound + case speechSynthesizer + case movementSoundAndSpeechSynthesizer + case none + + var displayDescription: String { + switch self { + case .movementSound: + return L10n.AudioMode.movementSound + + case .speechSynthesizer: + return L10n.AudioMode.speechSynthesizer + + case .movementSoundAndSpeechSynthesizer: + return L10n.AudioMode.movementSoundAndSpeechSynthesizer + + case .none: + return L10n.AudioMode.none + } + } +} diff --git a/App/Sources/Models/Position.swift b/App/Sources/Models/Position.swift index 4c2fb25..c072f37 100644 --- a/App/Sources/Models/Position.swift +++ b/App/Sources/Models/Position.swift @@ -14,10 +14,8 @@ enum Position: Int { case salamRight = 49 case salamLeft = 51 - // MARK: - Type Properties private static let baseMovementDuration = Duration.milliseconds(1_400) - // MARK: - Case Methods func arrow(forChangingTo newPosition: Position?) -> Arrow? { guard let newPosition = newPosition else { return nil } @@ -56,7 +54,6 @@ enum Position: Int { } } -// MARK: - Sub Types extension Position { enum Arrow: String { case upwards = "↑" diff --git a/App/Sources/Models/Prayer.swift b/App/Sources/Models/Prayer.swift index 7bc7bbe..c54022e 100644 --- a/App/Sources/Models/Prayer.swift +++ b/App/Sources/Models/Prayer.swift @@ -9,10 +9,8 @@ import SwiftyUserDefaults /// The "physical, mental, and spiritual act of worship that is observed five times every day at prescribed times." /// - Wikipedia (https://en.wikipedia.org/wiki/Salah) class Prayer { - // MARK: - Stored Instance Properties let rakat: Rakat - // MARK: - Initializer /// Creates a new prayer automatically based on the number of rakat. /// Currently the logic covers creating the five daily prayers. /// diff --git a/App/Sources/Models/PrayerState.swift b/App/Sources/Models/PrayerState.swift index 2064eb6..9538fc1 100644 --- a/App/Sources/Models/PrayerState.swift +++ b/App/Sources/Models/PrayerState.swift @@ -6,11 +6,12 @@ import UIKit class PrayerState { - // MARK: - Stored Instance Properties private let prayer: Prayer private let changingTextSpeedFactor: Double private let fixedTextsSpeedFactor: Double + private let audioMode: AudioMode private let movementSoundInstrument: String + private let speechSynthesizer: SpeechSynthesizer private var rakatIndex: Int = 0 private var componentIndex: Int = 0 @@ -22,20 +23,22 @@ class PrayerState { private var previousPositon: Position = .standing private var currentPosition: Position = .standing - // MARK: - Initializers init( prayer: Prayer, changingTextSpeedFactor: Double, fixedTextsSpeedFactor: Double, - movementSoundInstrument: String + audioMode: AudioMode, + movementSoundInstrument: String, + speechSynthesizer: SpeechSynthesizer ) { self.prayer = prayer self.changingTextSpeedFactor = changingTextSpeedFactor self.fixedTextsSpeedFactor = fixedTextsSpeedFactor + self.audioMode = audioMode self.movementSoundInstrument = movementSoundInstrument + self.speechSynthesizer = speechSynthesizer } - // MARK: - Computed Instance Properties private var currentRakah: Rakah { prayer.rakat[rakatIndex] } private var currentComponent: RakahComponent { currentRakah.components()[componentIndex] } var currentArrow: Position.Arrow? { previousPositon.arrow(forChangingTo: currentPosition) } @@ -48,13 +51,18 @@ class PrayerState { var currentLineReadingTime: TimeInterval { var readingTime = currentLine.estimatedReadingTime / readingSpeedupFactor - if lineIndex == 0 && currentComponent.needsMovement { - readingTime += previousPositon.movementDuration(forChangingTo: currentPosition) + if let movementDelay = movementDelay { + readingTime += movementDelay } return readingTime } + var movementDelay: TimeInterval? { + guard lineIndex == 0 && currentComponent.needsMovement else { return nil } + return previousPositon.movementDuration(forChangingTo: currentPosition) + } + var currentMovementSoundUrl: URL? { guard let movementSound = currentComponent.movementSound else { return nil } return AudioPlayer.shared.movementSoundUrl(name: movementSound, instrument: movementSoundInstrument) @@ -89,7 +97,6 @@ class PrayerState { return AudioPlayer.shared.movementSoundUrl(name: movementSound, instrument: movementSoundInstrument) } - // MARK: - Instance Methods func moveToNextLine() -> Bool { previousLine = currentLine @@ -122,8 +129,7 @@ class PrayerState { currentIsComponentBeginning: lineIndex == 0, nextArrow: nextArrow, nextLine: nextLine, - nextIsComponentBeginning: lineIndex + 1 == currentComponent.spokenTextLines.count, - movementSoundUrl: movementSoundUrl + nextIsComponentBeginning: lineIndex + 1 == currentComponent.spokenTextLines.count ) } } diff --git a/App/Sources/Models/Rakah.swift b/App/Sources/Models/Rakah.swift index bed2c02..563475d 100644 --- a/App/Sources/Models/Rakah.swift +++ b/App/Sources/Models/Rakah.swift @@ -13,7 +13,6 @@ typealias Rakat = [Rakah] /// "(A) single unit of Islamic prayers." /// - Wikipedia (https://en.wikipedia.org/wiki/Rakat) class Rakah { - // MARK: - Stored Instance Properties private let isBeginningOfPrayer: Bool private let includesSittingRecitation: Bool private let isEndOfPrayer: Bool @@ -21,7 +20,6 @@ class Rakah { private var includesStandingRecitation: Bool { standingRecitationPart != nil } - // MARK: - Initializer init( isBeginningOfPrayer: Bool, standingRecitationPart: RecitationPart?, @@ -34,7 +32,6 @@ class Rakah { self.standingRecitationPart = standingRecitationPart } - // MARK: - Instance Methods func components() -> [RakahComponent] { var components: [RakahComponent] = [] @@ -84,7 +81,6 @@ class Rakah { } } -// MARK: - Sub Types extension Rakah { enum Component { case takbir(Position) diff --git a/App/Sources/Models/RakahComponent.swift b/App/Sources/Models/RakahComponent.swift index 21275d7..31825bb 100644 --- a/App/Sources/Models/RakahComponent.swift +++ b/App/Sources/Models/RakahComponent.swift @@ -10,10 +10,8 @@ typealias Duration = DispatchTimeInterval /// A single unit a rakah can consists of. class RakahComponent { - // MARK: - Stored Type Properties static let durationPerCharacter = Timespan.milliseconds(55) - // MARK: - Computed Type Properties static var movementSoundInstrument: String { get { guard let instrument = UserDefaults.standard.string(forKey: "MovementSoundInstrument") else { @@ -28,7 +26,6 @@ class RakahComponent { } } - // MARK: - Stored Instance Properties let name: String let chapterNumber: Int? let spokenTextLines: [String] @@ -39,7 +36,6 @@ class RakahComponent { let l10n = L10n.RakahComponent.self - // MARK: - Initializers init( _ component: Rakah.Component, longSitting: Bool = false @@ -90,13 +86,11 @@ class RakahComponent { case let .recitationPart(recitationPart): chapterNumber = recitationPart.recitation.rawValue - let title = recitationPart.recitation.localizedTitle + var title = recitationPart.recitation.localizedTitle if recitationPart.totalParts > 1 { - name = l10n.splitRecitationTitle(title, recitationPart.part, recitationPart.totalParts) - } - else { - name = title + title = l10n.splitRecitationTitle(title, recitationPart.part, recitationPart.totalParts) } + name = "📖\(chapterNumber!): \(title)" spokenTextLines = RakahComponent.readLinesFromRecitationFile(recitationPart: recitationPart) needsMovement = false @@ -169,7 +163,6 @@ class RakahComponent { } } - // MARK: - Type Methods private static func readLinesFromFile(named name: String) -> [String] { let spokenTextFilePath = Bundle.main.url(forResource: name, withExtension: "txt")! let contentString = try! String(contentsOf: spokenTextFilePath, encoding: .utf8) @@ -192,7 +185,6 @@ class RakahComponent { } } -// MARK: - Sub Types extension String { var estimatedReadingTime: Timespan { RakahComponent.durationPerCharacter * Double(utf8.count) + .milliseconds(500) // add time for context switch diff --git a/App/Sources/ScreenFlows/Prayer/PrayerFlowController.swift b/App/Sources/ScreenFlows/Prayer/PrayerFlowController.swift index b6d2990..55b02b2 100644 --- a/App/Sources/ScreenFlows/Prayer/PrayerFlowController.swift +++ b/App/Sources/ScreenFlows/Prayer/PrayerFlowController.swift @@ -3,16 +3,17 @@ // Copyright © 2017 Flinesoft. All rights reserved. // +import AVKit import HandySwift import Imperio import UIKit class PrayerFlowController: FlowController { - // MARK: - Stored Instance Properties private let prayer: Prayer private let fixedTextSpeedsFactor: Double private let changingTextSpeedFactor: Double private let showChangingTextName: Bool + private var audioMode: AudioMode private let movementSoundInstrument: String private var prayerState: PrayerState! @@ -20,23 +21,26 @@ class PrayerFlowController: FlowController { private var countdown: Countdown? private var timer: Timer? + private var speechSynthesizer: SpeechSynthesizer - // MARK: - Initializers init( prayer: Prayer, fixedTextSpeedsFactor: Double, changingTextSpeedFactor: Double, showChangingTextName: Bool, - movementSoundInstrument: String + audioMode: AudioMode, + movementSoundInstrument: String, + speechSynthesizer: SpeechSynthesizer ) { self.prayer = prayer self.fixedTextSpeedsFactor = fixedTextSpeedsFactor self.changingTextSpeedFactor = changingTextSpeedFactor self.showChangingTextName = showChangingTextName + self.audioMode = audioMode self.movementSoundInstrument = movementSoundInstrument + self.speechSynthesizer = speechSynthesizer } - // MARK: - Instance Methods override func start(from presentingViewController: UIViewController) { // configure prayer view controller prayerViewCtrl = StoryboardScene.PrayerView.initialScene.instantiate() @@ -48,6 +52,14 @@ class PrayerFlowController: FlowController { countdown? .onCount { count in self.prayerViewCtrl.viewModel = self.countdownViewModel(count: count) + + switch self.audioMode { + case .speechSynthesizer, .movementSoundAndSpeechSynthesizer: + self.speechSynthesizer.speak(text: String(count)) + + case .movementSound, .none: + break + } } countdown?.onFinish { self.startPrayer() } @@ -57,6 +69,14 @@ class PrayerFlowController: FlowController { presentingViewController.present(navCtrl, animated: true) { self.prayerViewCtrl.viewModel = self.countdownViewModel(count: countdownCount) self.countdown?.start() + + switch self.audioMode { + case .speechSynthesizer, .movementSoundAndSpeechSynthesizer: + self.speechSynthesizer.speak(text: String(countdownCount)) + + case .movementSound, .none: + break + } } } @@ -71,8 +91,7 @@ class PrayerFlowController: FlowController { currentIsComponentBeginning: false, nextArrow: nil, nextLine: nil, - nextIsComponentBeginning: true, - movementSoundUrl: nil + nextIsComponentBeginning: true ) } @@ -81,38 +100,75 @@ class PrayerFlowController: FlowController { prayer: prayer, changingTextSpeedFactor: changingTextSpeedFactor, fixedTextsSpeedFactor: fixedTextSpeedsFactor, - movementSoundInstrument: movementSoundInstrument + audioMode: audioMode, + movementSoundInstrument: movementSoundInstrument, + speechSynthesizer: speechSynthesizer ) prayerViewCtrl.viewModel = prayerState.prayerViewModel() progressPrayer() + // set audio session to this app + try? AVAudioSession.sharedInstance().setActive(true) + // prevent screen from locking UIApplication.shared.isIdleTimerDisabled = true } func progressPrayer() { - timer = Timer.after(prayerState.currentLineReadingTime) { - if self.prayerState.moveToNextLine() { - let viewModel = self.prayerState.prayerViewModel() - - // show changing text info if chosen - if self.showChangingTextName && viewModel.currentIsComponentBeginning { - if let chapterNum = self.prayerState.currentRecitationChapterNum { - let infoViewModel = PrayerViewModel( - currentComponentName: viewModel.currentComponentName, - previousArrow: viewModel.previousArrow, - previousLine: viewModel.previousLine, - currentArrow: nil, - currentLine: "📖\(chapterNum): \(viewModel.currentComponentName)", - isChapterName: true, - currentIsComponentBeginning: true, - nextArrow: nil, - nextLine: viewModel.currentLine, - nextIsComponentBeginning: false, - movementSoundUrl: viewModel.movementSoundUrl - ) - self.prayerViewCtrl.viewModel = infoViewModel + switch audioMode { + case .movementSound: + if let movementSoundUrl = prayerState.currentMovementSoundUrl { + AudioPlayer.shared.playSound(at: movementSoundUrl) + } + + timer = Timer.after(prayerState.currentLineReadingTime, progressToNextStep) + + case .speechSynthesizer: + speechSynthesizer.speak( + text: prayerState.currentLine, + completion: progressToNextStep, + delayCompletion: prayerState.movementDelay + ) + + case .movementSoundAndSpeechSynthesizer: + if let movementSoundUrl = prayerState.currentMovementSoundUrl { + AudioPlayer.shared.playSound(at: movementSoundUrl) + } + speechSynthesizer.speak( + text: prayerState.currentLine, + completion: progressToNextStep, + delayCompletion: prayerState.movementDelay + ) + + case .none: + timer = Timer.after(prayerState.currentLineReadingTime, progressToNextStep) + } + } + + private func progressToNextStep() { + if prayerState.moveToNextLine() { + let viewModel = prayerState.prayerViewModel() + + // show changing text info if chosen + if self.showChangingTextName && viewModel.currentIsComponentBeginning { + if let chapterNum = prayerState.currentRecitationChapterNum, chapterNum != 1 { + let infoViewModel = PrayerViewModel( + currentComponentName: viewModel.currentComponentName, + previousArrow: viewModel.previousArrow, + previousLine: viewModel.previousLine, + currentArrow: nil, + currentLine: viewModel.currentComponentName, + isChapterName: true, + currentIsComponentBeginning: true, + nextArrow: nil, + nextLine: viewModel.currentLine, + nextIsComponentBeginning: false + ) + self.prayerViewCtrl.viewModel = infoViewModel + + switch audioMode { + case .movementSound, .none: let rememberTime = Timespan.milliseconds(1_000) let waitTime = infoViewModel.currentLine.estimatedReadingTime + rememberTime delay(by: waitTime) { @@ -120,24 +176,32 @@ class PrayerFlowController: FlowController { self.progressPrayer() } - return + case .speechSynthesizer, .movementSoundAndSpeechSynthesizer: + speechSynthesizer.speak(text: infoViewModel.currentLine) { + self.prayerViewCtrl.viewModel = self.prayerState.prayerViewModel() + self.progressPrayer() + } } - } - self.prayerViewCtrl.viewModel = self.prayerState.prayerViewModel() - self.progressPrayer() - } - else { - self.cleanup() - self.prayerViewCtrl.dismiss(animated: true, completion: nil) - self.removeFromSuperFlowController() + return + } } + + prayerViewCtrl.viewModel = prayerState.prayerViewModel() + progressPrayer() + } + else { + cleanup() + prayerViewCtrl.dismiss(animated: true, completion: nil) + removeFromSuperFlowController() } } - func cleanup() { + private func cleanup() { timer?.invalidate() timer = nil + speechSynthesizer.stop() + try? AVAudioSession.sharedInstance().setActive(false) UIApplication.shared.isIdleTimerDisabled = false } } diff --git a/App/Sources/ScreenFlows/Prayer/PrayerViewController.swift b/App/Sources/ScreenFlows/Prayer/PrayerViewController.swift index a9be6b2..ecd02e5 100644 --- a/App/Sources/ScreenFlows/Prayer/PrayerViewController.swift +++ b/App/Sources/ScreenFlows/Prayer/PrayerViewController.swift @@ -13,7 +13,6 @@ protocol PrayerFlowDelegate: AnyObject { } class PrayerViewController: UIViewController { - // MARK: - Stored Instance Properties var viewModel: PrayerViewModel! { didSet { title = viewModel.currentComponentName @@ -28,16 +27,11 @@ class PrayerViewController: UIViewController { else { currentLineLabel.textColor = Colors.primary } - - if let movementSoundUrl = viewModel.movementSoundUrl { - AudioPlayer.shared.playSound(at: movementSoundUrl) - } } } weak var flowDelegate: PrayerFlowDelegate? - // MARK: - IBOutlets @IBOutlet private var previousLineLabel: UILabel! @IBOutlet private var currentLineLabel: UILabel! @IBOutlet private var nextLineLabel: UILabel! @@ -49,7 +43,6 @@ class PrayerViewController: UIViewController { @IBOutlet private var currentLineComponentSeparator: UIImageView! @IBOutlet private var nextLineComponentSeparator: UIImageView! - // MARK: - Instance Methods override func viewDidLoad() { super.viewDidLoad() diff --git a/App/Sources/ScreenFlows/Prayer/PrayerViewModel.swift b/App/Sources/ScreenFlows/Prayer/PrayerViewModel.swift index 82727dd..e2cdfe8 100644 --- a/App/Sources/ScreenFlows/Prayer/PrayerViewModel.swift +++ b/App/Sources/ScreenFlows/Prayer/PrayerViewModel.swift @@ -20,6 +20,4 @@ struct PrayerViewModel { let nextArrow: Position.Arrow? let nextLine: String? let nextIsComponentBeginning: Bool - - let movementSoundUrl: URL? } diff --git a/App/Sources/ScreenFlows/Settings/AudioVolumeView.swift b/App/Sources/ScreenFlows/Settings/AudioVolumeView.swift new file mode 100644 index 0000000..bedef2b --- /dev/null +++ b/App/Sources/ScreenFlows/Settings/AudioVolumeView.swift @@ -0,0 +1,57 @@ +// +// Created by Cihat Gündüz on 30.10.21. +// Copyright © 2021 Flinesoft. All rights reserved. +// + +import AVKit +import UIKit +import HandyUIKit + +class AudioVolumeView: UIView { + private let currentPortLabel: UILabel + private let deviceChooserButton: AVRoutePickerView + + override init( + frame: CGRect + ) { + deviceChooserButton = .init(frame: .init(x: 36, y: 0, width: 25, height: frame.height)) + deviceChooserButton.prioritizesVideoDevices = false + + currentPortLabel = .init(frame: .init(size: frame.size)) + currentPortLabel.textColor = .secondaryLabel + currentPortLabel.textAlignment = .right + + super.init(frame: frame) + + addSubview(deviceChooserButton) + addSubview(currentPortLabel) + currentPortLabel.bindEdgesToSuperview() + + updateCurrentPortLabel() + subscribeToRouteChangeNotification() + } + + required init?( + coder: NSCoder + ) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc + private func updateCurrentPortLabel() { + currentPortLabel.text = AVAudioSession.sharedInstance().currentRoute.outputs.first?.portName + } + + private func subscribeToRouteChangeNotification() { + NotificationCenter.default.addObserver( + self, + selector: #selector(updateCurrentPortLabel), + name: AVAudioSession.routeChangeNotification, + object: nil + ) + } +} diff --git a/App/Sources/ScreenFlows/Settings/FAQCollectionViewCell.swift b/App/Sources/ScreenFlows/Settings/FAQCollectionViewCell.swift index 66d8c1b..35cc9e5 100644 --- a/App/Sources/ScreenFlows/Settings/FAQCollectionViewCell.swift +++ b/App/Sources/ScreenFlows/Settings/FAQCollectionViewCell.swift @@ -6,7 +6,6 @@ import UIKit class FAQCollectionViewCell: UICollectionViewCell { - // MARK: - IBOutlets @IBOutlet var questionLabel: UILabel! @IBOutlet var answerLabel: UILabel! } diff --git a/App/Sources/ScreenFlows/Settings/FAQCollectionViewLayout.swift b/App/Sources/ScreenFlows/Settings/FAQCollectionViewLayout.swift index 1e6dc3a..d94acc4 100644 --- a/App/Sources/ScreenFlows/Settings/FAQCollectionViewLayout.swift +++ b/App/Sources/ScreenFlows/Settings/FAQCollectionViewLayout.swift @@ -13,17 +13,14 @@ protocol FAQCollectionViewLayoutDelegate: AnyObject { @IBDesignable class FAQCollectionViewLayout: UICollectionViewLayout { - // MARK: - Stored Instance Properties var questionLabelFont = UIFont.systemFont(ofSize: 16, weight: UIFont.Weight.bold) var answerLabelFont = UIFont.systemFont(ofSize: 16, weight: UIFont.Weight.regular) private var cachedLayoutAttributes: [UICollectionViewLayoutAttributes] = [] private var contentHeight: CGFloat = 0 - // MARK: - IBOutlets weak var delegate: FAQCollectionViewLayoutDelegate? - // MARK: - Computed Instance Properties var columns: Int { var columns = Int(collectionView!.bounds.size.width) / preferredItemWidth if Int(collectionView!.bounds.size.width) % preferredItemWidth > preferredItemWidth / 2 { @@ -40,11 +37,9 @@ class FAQCollectionViewLayout: UICollectionViewLayout { return (collectionView!.bounds.size.width - allGapsWidth) / CGFloat(columns) } - // MARK: - IBInspectables @IBInspectable var preferredItemWidth: Int = 400 @IBInspectable var gapWidth: CGFloat = UIDevice.current.userInterfaceIdiom == .pad ? 30 : 12 - // MARK: - Instance Methods override func prepare() { guard let collectionView = collectionView else { log.info("Skipping FAQ layout preparation."); return diff --git a/App/Sources/ScreenFlows/Settings/FAQViewController.swift b/App/Sources/ScreenFlows/Settings/FAQViewController.swift index aad75b3..25eedf0 100644 --- a/App/Sources/ScreenFlows/Settings/FAQViewController.swift +++ b/App/Sources/ScreenFlows/Settings/FAQViewController.swift @@ -12,10 +12,8 @@ protocol FAQFlowDelegate: AnyObject { } class FAQViewController: UIViewController { - // MARK: - IBOutlets @IBOutlet private var collectionView: UICollectionView! - // MARK: - Stored Instance Properties private let l10n = L10n.Settings.Faq.self fileprivate let cellReuseIdentifier: String = "FAQCell" @@ -29,7 +27,6 @@ class FAQViewController: UIViewController { } } - // MARK: - View Lifecycle Methods override func viewDidLoad() { super.viewDidLoad() @@ -37,13 +34,11 @@ class FAQViewController: UIViewController { (collectionView.collectionViewLayout as! FAQCollectionViewLayout).delegate = self } - // MARK: - IBAction Methods @IBAction private func doneButtonPressed() { flowDelegate?.doneButtonPressed() } } -// MARK: - UICollectionViewDataSource Protocol Implementation extension FAQViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { viewModel.entries.count @@ -64,7 +59,6 @@ extension FAQViewController: UICollectionViewDataSource { } } -// MARK: - FAQCollectionViewLayoutDelegate Protocol Implementation extension FAQViewController: FAQCollectionViewLayoutDelegate { func question(at indexPath: IndexPath) -> String { viewModel.entries[indexPath.item].question diff --git a/App/Sources/ScreenFlows/Settings/FAQViewModel.swift b/App/Sources/ScreenFlows/Settings/FAQViewModel.swift index dba57ba..4ae0b8e 100644 --- a/App/Sources/ScreenFlows/Settings/FAQViewModel.swift +++ b/App/Sources/ScreenFlows/Settings/FAQViewModel.swift @@ -6,9 +6,7 @@ import UIKit struct FAQViewModel { - // MARK: - Sub Types typealias FAQEntry = (question: String, answer: String) - // MARK: - Stored Instance Properties let entries: [FAQEntry] } diff --git a/App/Sources/ScreenFlows/Settings/SettingsFlowController.swift b/App/Sources/ScreenFlows/Settings/SettingsFlowController.swift index 47d2a07..a8150df 100644 --- a/App/Sources/ScreenFlows/Settings/SettingsFlowController.swift +++ b/App/Sources/ScreenFlows/Settings/SettingsFlowController.swift @@ -3,19 +3,18 @@ // Copyright © 2017 Flinesoft. All rights reserved. // +import AVKit import Imperio import SafariServices import SwiftyUserDefaults import UIKit class SettingsFlowController: InitialFlowController { - // MARK: - Stored Instance Properties private let l10n = L10n.Settings.self var settingsViewModel: SettingsViewModel! var settingsViewCtrl: SettingsViewController! var faqViewCtrl: FAQViewController? - // MARK: - Coordinator Methods override func start(from window: UIWindow) { settingsViewModel = SettingsViewModel() settingsViewCtrl = SettingsViewController(viewModel: settingsViewModel) @@ -55,6 +54,22 @@ extension SettingsFlowController: SettingsFlowDelegate { settingsViewModel.allowSplittingRecitations = allowSplittingRecitations } + func setSpeechSynthesizerVoice(_ voice: AVSpeechSynthesisVoice) { + settingsViewModel.speechSynthesizerVoice = voice + } + + func setSpeechSynthesizerSpeechRate(_ speechRate: Float) { + settingsViewModel.speechSynthesizerSpeechRate = speechRate + } + + func setSpeechSynthesizerPitchMultiplier(_ pitchMultiplier: Float) { + settingsViewModel.speechSynthesizerPitchMultiplier = pitchMultiplier + } + + func setAudioMode(_ audioMode: AudioMode) { + settingsViewModel.audioMode = audioMode + } + func showLanguageSettings() { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) } @@ -83,16 +98,18 @@ extension SettingsFlowController: SettingsFlowDelegate { allowSplittingRecitations: settingsViewModel.allowSplittingRecitations ) - let prayerCoordinator = PrayerFlowController( + let prayerFlowCtrl = PrayerFlowController( prayer: prayer, fixedTextSpeedsFactor: settingsViewModel.fixedTextsSpeedFactor, changingTextSpeedFactor: settingsViewModel.changingTextSpeedFactor, showChangingTextName: settingsViewModel.showChangingTextName, - movementSoundInstrument: settingsViewModel.movementSoundInstrument + audioMode: settingsViewModel.audioMode, + movementSoundInstrument: settingsViewModel.movementSoundInstrument, + speechSynthesizer: settingsViewModel.speechSynthesizer ) - add(subFlowController: prayerCoordinator) - prayerCoordinator.start(from: settingsViewCtrl) + add(subFlowController: prayerFlowCtrl) + prayerFlowCtrl.start(from: settingsViewCtrl) } private func showFAQ() { @@ -105,7 +122,6 @@ extension SettingsFlowController: SettingsFlowDelegate { (question: localL10n.AppMotivation.question, answer: localL10n.AppMotivation.answer), (question: localL10n.IpadReading.question, answer: localL10n.IpadReading.answer), (question: localL10n.Language.question, answer: localL10n.Language.answer), - (question: localL10n.LanguageMix.question, answer: localL10n.LanguageMix.answer), (question: localL10n.TranslationProblem.question, answer: localL10n.TranslationProblem.answer), ] ) diff --git a/App/Sources/ScreenFlows/Settings/SettingsViewController.swift b/App/Sources/ScreenFlows/Settings/SettingsViewController.swift index 9195e3f..3f7ec25 100644 --- a/App/Sources/ScreenFlows/Settings/SettingsViewController.swift +++ b/App/Sources/ScreenFlows/Settings/SettingsViewController.swift @@ -3,11 +3,16 @@ // Copyright © 2017 Flinesoft. All rights reserved. // +// 4. Speaker / Bluetooth chooser + +import AVKit import Eureka import HandyUIKit +import HandySwift import Imperio -import UIKit import SwiftyUserDefaults +import UIKit +import ViewRow protocol SettingsFlowDelegate: AnyObject { func setRakat(_ rakatCount: Int) @@ -16,6 +21,10 @@ protocol SettingsFlowDelegate: AnyObject { func setShowChangingTextName(_ showChangingTextName: Bool) func setAllowLongerRecitations(_ allowLongerRecitations: Bool) func setAllowSplittingRecitations(_ allowSplittingRecitations: Bool) + func setSpeechSynthesizerVoice(_ voice: AVSpeechSynthesisVoice) + func setSpeechSynthesizerPitchMultiplier(_ pitchMultiplier: Float) + func setSpeechSynthesizerSpeechRate(_ speechRate: Float) + func setAudioMode(_ audioMode: AudioMode) func showLanguageSettings() func chooseInstrument(_ instrument: String) func startPrayer() @@ -24,13 +33,19 @@ protocol SettingsFlowDelegate: AnyObject { } class SettingsViewController: FormViewController { - // MARK: - Stored Instance Properties let l10n = L10n.Settings.self var viewModel: SettingsViewModel + private let audioModeRowTag: String = "AudioMode" + weak var flowDelegate: SettingsFlowDelegate? - // MARK: - Initializers + private var audioMode: AudioMode? { + UIDevice.current.userInterfaceIdiom == .pad + ? (form.rowBy(tag: self.audioModeRowTag) as! SegmentedRow).value + : (form.rowBy(tag: self.audioModeRowTag) as! PushRow).value + } + init( viewModel: SettingsViewModel ) { @@ -46,7 +61,6 @@ class SettingsViewController: FormViewController { fatalError("init(coder:) has not been implemented") } - // MARK: - View Lifecycle Methods override func viewDidLoad() { super.viewDidLoad() @@ -54,20 +68,21 @@ class SettingsViewController: FormViewController { setupAppSection() setupPrayerSection() + setupAudioAndSpeedSection() setupStartSection() setupFAQButton() setupFeedbackButton() } - // MARK: - Instance Methods private func setupAppSection() { let appSection = - Section(l10n.AppSection.title) + Section(header: l10n.AppSection.title, footer: l10n.AppSection.footer) <<< ButtonRow { row in row.title = l10n.AppSection.ChangeLanguageButton.title } .cellSetup { cell, _ in cell.textLabel?.font = UIFont.systemFont(ofSize: 18) + cell.imageView?.image = UIImage(systemName: "flag") } .onCellSelection { _, _ in self.flowDelegate?.showLanguageSettings() @@ -78,23 +93,70 @@ class SettingsViewController: FormViewController { private func setupPrayerSection() { let prayerSection = - Section(l10n.PrayerSection.title) + Section(header: l10n.PrayerSection.title, footer: l10n.PrayerSection.footer) <<< rakatCountRow() - <<< fixedTextsRow() - <<< changingTextRow() <<< changingTextNameRow() <<< allowLongerRecitationsRow() <<< allowSplittingRecitationsRow() - <<< movementSoundInstrumentRow() form.append(prayerSection) } - fileprivate func rakatCountRow() -> IntRow { - return IntRow { row in + private func setupAudioAndSpeedSection() { + let audioAndSpeedSection = + Section(header: l10n.AudioSpeedSection.title, footer: l10n.AudioSpeedSection.footer) + <<< (UIDevice.current.userInterfaceIdiom == .pad ? audioModeSegmentedRow() : audioModePushRow()) + <<< fixedTextSpeedRow() + <<< changingTextSpeedRow() + <<< movementSoundInstrumentRow() + <<< speechSynthesizerVoiceRow() + <<< speechSynthesizerSpeechRateRow() + <<< speechSynthesizerPitchMultiplierRow() + <<< volumeViewRow() + + form.append(audioAndSpeedSection) + } + + private func audioModePushRow() -> PushRow { + PushRow(audioModeRowTag) { row in + row.title = l10n.AudioSpeedSection.AudioMode.title + row.options = AudioMode.allCases + row.value = viewModel.audioMode + row.displayValueFor = { $0?.displayDescription } + } + .cellSetup { cell, _ in + cell.imageView?.image = UIImage(systemName: "waveform") + } + .onChange { row in + guard let rowValue = row.value else { log.error("Audio mode row had nil value."); return } + self.flowDelegate?.setAudioMode(rowValue) + } + } + + private func audioModeSegmentedRow() -> SegmentedRow { + SegmentedRow(audioModeRowTag) { row in + row.title = l10n.AudioSpeedSection.AudioMode.title + row.options = AudioMode.allCases + row.value = viewModel.audioMode + row.displayValueFor = { $0?.displayDescription } + } + .cellSetup { cell, _ in + cell.imageView?.image = UIImage(systemName: "waveform") + } + .onChange { row in + guard let rowValue = row.value else { log.error("Audio mode row had nil value."); return } + self.flowDelegate?.setAudioMode(rowValue) + } + } + + private func rakatCountRow() -> IntRow { + IntRow { row in row.title = l10n.PrayerSection.RakatCount.title row.value = viewModel.rakatCount } + .cellSetup { cell, _ in + cell.imageView?.image = UIImage(systemName: "number") + } .onCellHighlightChanged { cell, row in if cell.textField.isFirstResponder { row.value = nil @@ -106,13 +168,17 @@ class SettingsViewController: FormViewController { } } - fileprivate func fixedTextsRow() -> SliderRow { - return SliderRow { row in - row.title = l10n.PrayerSection.FixedTexts.title + private func fixedTextSpeedRow() -> SliderRow { + SliderRow { row in + row.title = "🔂 " + l10n.AudioSpeedSection.FixedTexts.title row.value = Float(viewModel.fixedTextsSpeedFactor) + row.displayValueFor = { String(format: "%.2f", $0!) } row.cell.slider.minimumValue = 0.5 row.cell.slider.maximumValue = 2.0 row.steps = UInt((row.cell.slider.maximumValue - row.cell.slider.minimumValue) / 0.05) + row.hidden = Condition.function([audioModeRowTag]) { _ in + self.audioMode != .movementSound && self.audioMode != AudioMode.none + } } .onChange { row in guard let rowValue = row.value else { log.error("Fixed text speed had nil value."); return } @@ -120,13 +186,17 @@ class SettingsViewController: FormViewController { } } - fileprivate func changingTextRow() -> SliderRow { - return SliderRow { row in - row.title = l10n.PrayerSection.ChangingText.title + private func changingTextSpeedRow() -> SliderRow { + SliderRow { row in + row.title = "🔀 " + l10n.AudioSpeedSection.ChangingText.title row.value = Float(viewModel.changingTextSpeedFactor) + row.displayValueFor = { String(format: "%.2f", $0!) } row.cell.slider.minimumValue = 0.5 row.cell.slider.maximumValue = 2.0 row.steps = UInt((row.cell.slider.maximumValue - row.cell.slider.minimumValue) / 0.05) + row.hidden = Condition.function([audioModeRowTag]) { _ in + self.audioMode != .movementSound && self.audioMode != AudioMode.none + } } .onChange { row in guard let rowValue = row.value else { log.error("Changing text speed had nil value."); return } @@ -134,22 +204,28 @@ class SettingsViewController: FormViewController { } } - fileprivate func changingTextNameRow() -> SwitchRow { - return SwitchRow { row in + private func changingTextNameRow() -> SwitchRow { + SwitchRow { row in row.title = l10n.PrayerSection.ChangingTextName.title row.value = viewModel.showChangingTextName } + .cellSetup { cell, _ in + cell.imageView?.image = UIImage(systemName: "character.book.closed") + } .onChange { row in guard let rowValue = row.value else { log.error("Show changing text name had nil value."); return } self.flowDelegate?.setShowChangingTextName(rowValue) } } - fileprivate func allowLongerRecitationsRow() -> SwitchRow { - return SwitchRow { row in + private func allowLongerRecitationsRow() -> SwitchRow { + SwitchRow { row in row.title = l10n.PrayerSection.AllowLongerRecitations.title row.value = viewModel.allowLongerRecitations } + .cellSetup { cell, _ in + cell.imageView?.image = UIImage(systemName: "clock.badge.checkmark") + } .onChange { row in guard let rowValue = row.value else { log.error("Allow longer recitations had nil value."); return } @@ -165,22 +241,31 @@ class SettingsViewController: FormViewController { } } - fileprivate func allowSplittingRecitationsRow() -> SwitchRow { - return SwitchRow { row in + private func allowSplittingRecitationsRow() -> SwitchRow { + SwitchRow { row in row.title = l10n.PrayerSection.AllowSplittingRecitations.title row.value = viewModel.allowSplittingRecitations } + .cellSetup { cell, _ in + cell.imageView?.image = UIImage(systemName: "scissors") + } .onChange { row in guard let rowValue = row.value else { log.error("Allow splitting recitations had nil value."); return } self.flowDelegate?.setAllowSplittingRecitations(rowValue) } } - fileprivate func movementSoundInstrumentRow() -> PushRow { - return PushRow { row in - row.title = l10n.PrayerSection.MovementSoundInstrument.title + private func movementSoundInstrumentRow() -> PushRow { + PushRow { row in + row.title = l10n.AudioSpeedSection.MovementSoundInstrument.title row.options = SettingsViewModel.availableMovementSoundInstruments row.value = viewModel.movementSoundInstrument + row.hidden = Condition.function([audioModeRowTag]) { _ in + self.audioMode != .movementSound && self.audioMode != .movementSoundAndSpeechSynthesizer + } + } + .cellSetup { cell, _ in + cell.imageView?.image = UIImage(systemName: "guitars") } .onChange { row in guard let rowValue = row.value else { log.error("Instrument had nil value."); return } @@ -188,11 +273,100 @@ class SettingsViewController: FormViewController { } } + private func speechSynthesizerVoiceRow() -> PushRow { + PushRow { row in + row.title = l10n.Audio.SpeechSynthesizer.voice + row.options = SpeechSynthesizer.SupportedLanguage.current.voices + row.value = viewModel.speechSynthesizerVoice + row.displayValueFor = { $0 != nil ? "\($0!.name) (\($0!.language))" : nil } + row.hidden = Condition.function([audioModeRowTag]) { _ in + self.audioMode != .speechSynthesizer && self.audioMode != .movementSoundAndSpeechSynthesizer + } + } + .cellSetup { cell, _ in + cell.imageView?.image = UIImage(systemName: "person.wave.2") + } + .onChange { row in + guard let rowValue = row.value else { log.error("Synthesizer voice had nil value."); return } + self.flowDelegate?.setSpeechSynthesizerVoice(rowValue) + } + .onPresent { from, to in + to.selectableRowCellUpdate = { cell, row in + cell.textLabel?.text = row.selectableValue!.name + cell.detailTextLabel?.text = Locale.current.localizedString(forIdentifier: row.selectableValue!.language) + } + } + } + + private func speechSynthesizerPitchMultiplierRow() -> SliderRow { + SliderRow { row in + row.title = "↕️ " + l10n.Audio.SpeechSynthesizer.pitchMultiplier + row.value = viewModel.speechSynthesizerPitchMultiplier + row.displayValueFor = { String(format: "%.2f", $0!) } + row.cell.slider.minimumValue = 0.5 + row.cell.slider.maximumValue = 2.0 + row.steps = UInt((row.cell.slider.maximumValue - row.cell.slider.minimumValue) / 0.05) + row.hidden = Condition.function([audioModeRowTag]) { _ in + self.audioMode != .speechSynthesizer && self.audioMode != .movementSoundAndSpeechSynthesizer + } + } + .onChange { row in + guard let rowValue = row.value else { log.error("Pitch multiplier had nil value."); return } + self.flowDelegate?.setSpeechSynthesizerPitchMultiplier(rowValue) + } + } + + private func speechSynthesizerSpeechRateRow() -> SliderRow { + SliderRow { row in + row.title = "🐇 " + l10n.Audio.SpeechSynthesizer.speechRate + row.value = viewModel.speechSynthesizerSpeechRate + row.displayValueFor = { String(format: "%.2f", $0!) } + row.cell.slider.minimumValue = (AVSpeechUtteranceMinimumSpeechRate + AVSpeechUtteranceDefaultSpeechRate) / 2 + row.cell.slider.maximumValue = (AVSpeechUtteranceMaximumSpeechRate + AVSpeechUtteranceDefaultSpeechRate) / 2 + row.steps = UInt((row.cell.slider.maximumValue - row.cell.slider.minimumValue) / 0.01) + row.hidden = Condition.function([audioModeRowTag]) { _ in + self.audioMode != .speechSynthesizer && self.audioMode != .movementSoundAndSpeechSynthesizer + } + } + .onChange { row in + guard let rowValue = row.value else { log.error("Speech rate had nil value."); return } + self.flowDelegate?.setSpeechSynthesizerSpeechRate(rowValue) + } + } + + private func volumeViewRow() -> ViewRow { + ViewRow { row in + row.title = "🔈 " + l10n.Audio.OutputDevice.title + row.hidden = Condition.function([audioModeRowTag]) { _ in + self.audioMode == nil || self.audioMode == AudioMode.none + } + } + .cellSetup { cell, _ in + let horizontalMargin: CGFloat = 20 + let verticalMargin: CGFloat = 10 + let volumeSliderHeight: CGFloat = 18 + + cell.view = AudioVolumeView( + frame: CGRect( + width: UIScreen.main.bounds.width - 2 * horizontalMargin, + height: volumeSliderHeight + 2 * verticalMargin + ) + ) + + cell.titleLeftMargin = horizontalMargin + cell.titleRightMargin = horizontalMargin + + cell.viewLeftMargin = horizontalMargin + cell.viewRightMargin = horizontalMargin + cell.viewBottomMargin = 5 + } + } + private func setupStartSection() { let startSection = Section() <<< ButtonRow { row in - row.title = L10n.Settings.StartButton.title + row.title = "🕋 " + L10n.Settings.StartButton.title } .cellSetup { cell, _ in cell.textLabel?.font = UIFont.systemFont(ofSize: 20, weight: UIFont.Weight.semibold) diff --git a/App/Sources/ScreenFlows/Settings/SettingsViewModel.swift b/App/Sources/ScreenFlows/Settings/SettingsViewModel.swift index 1152a62..f40e8cf 100644 --- a/App/Sources/ScreenFlows/Settings/SettingsViewModel.swift +++ b/App/Sources/ScreenFlows/Settings/SettingsViewModel.swift @@ -3,17 +3,16 @@ // Copyright © 2017 Flinesoft. All rights reserved. // +import AVKit import SwiftyUserDefaults import UIKit class SettingsViewModel { - // MARK: - Stored Instance Properties static let availableMovementSoundInstruments: [String] = [ "Baroque Organ", "Bleep City", "Erhu", "Flow Motion", "Grand Piano with Pad & Choir", "Infinite Space", "Persian Santoor", "Soft Waves", "Turkish Saz Zither", "Tweed Picked Synth", ] - // MARK: - Computed Instance Properties var rakatCount: Int { get { Defaults.rakatCount } set { Defaults.rakatCount = newValue } @@ -48,10 +47,40 @@ class SettingsViewModel { get { Defaults.movementSoundInstrument } set { Defaults.movementSoundInstrument = newValue } } + + var speechSynthesizerVoice: AVSpeechSynthesisVoice { + get { AVSpeechSynthesisVoice(identifier: Defaults.speechSynthesizerVoiceId)! } + set { Defaults.speechSynthesizerVoiceId = newValue.identifier } + } + + var speechSynthesizerPitchMultiplier: Float { + get { Float(Defaults.speechSynthesizerPitchMultiplier) } + set { Defaults.speechSynthesizerPitchMultiplier = Double(newValue) } + } + + var speechSynthesizerSpeechRate: Float { + get { Float(Defaults.speechSynthesizerSpeechRate) } + set { Defaults.speechSynthesizerSpeechRate = Double(newValue) } + } + + var speechSynthesizer: SpeechSynthesizer { + .init( + voice: speechSynthesizerVoice, + pitchMultiplier: speechSynthesizerPitchMultiplier, + speechRate: speechSynthesizerSpeechRate + ) + } + + var audioMode: AudioMode { + get { Defaults.audioMode } + set { Defaults.audioMode = newValue } + } } extension DefaultsKeys { private var defaultInstrument: String { SettingsViewModel.availableMovementSoundInstruments.first! } + private var defaultSpeechRate: Double { Double(AVSpeechUtteranceDefaultSpeechRate) } + private var defaultVoice: String { SpeechSynthesizer.SupportedLanguage.bestMatchingVoice.identifier } var rakatCount: DefaultsKey { .init("RakatCount", defaultValue: 4) } var fixedTextsSpeedFactor: DefaultsKey { .init("FixedTextsSpeedFactor", defaultValue: 1.0) } @@ -60,4 +89,10 @@ extension DefaultsKeys { var allowLongerRecitations: DefaultsKey { .init("AllowLongerRecitations", defaultValue: false) } var allowSplittingRecitations: DefaultsKey { .init("AllowSplittingRecitations", defaultValue: false) } var movementSoundInstrument: DefaultsKey { .init("MovementSoundInstrument", defaultValue: defaultInstrument) } + var speechSynthesizerVoiceId: DefaultsKey { .init("VoiceId", defaultValue: defaultVoice) } + var speechSynthesizerPitchMultiplier: DefaultsKey { .init("PitchMultiplier", defaultValue: 1.0) } + var speechSynthesizerSpeechRate: DefaultsKey { .init("SpeechRate", defaultValue: defaultSpeechRate) } + var audioMode: DefaultsKey { .init("AudioMode", defaultValue: .movementSound) } } + +extension AudioMode: DefaultsSerializable {} diff --git a/App/SupportingFiles/Info.plist b/App/SupportingFiles/Info.plist index 30ad32e..80ad628 100644 --- a/App/SupportingFiles/Info.plist +++ b/App/SupportingFiles/Info.plist @@ -20,6 +20,10 @@ $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + UIBackgroundModes + + audio + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d23272..97c76c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,18 @@ If needed, pluralize to `Issues`, `PRs` or `Authors` and list multiple separated ### Security - None. +## [1.3.0] - 2021-10-31 +### Added +- Adds a new audio mode where a computer voice reads out loud the full text of the prayer. +- Ensures the new spoken text audio mode continues to play when device is locked or app is in background. +- Shows the currently connected audio device and adds a button to switch the device right within the app. +- Adds detailed descriptions below each settings section to give some additional context. +### Changed +- Position change sound now ignores the systems Mute switch setting. Set audio mode to `muted` instead to pray without any sounds. +- All settings entries now have an icon for faster recognition and to make the settings screen look nicer. +### Fixed +- Adjusted green accent color in dark mode to be brighter for more legible text on buttons. + ## [1.2.1] - 2021-10-17 ### Fixed - Fixed an issue where the navigation bar of the prayer view had a black background in light mode. Also fixes the animation when opening it up. diff --git a/Prayer.xcodeproj/project.pbxproj b/Prayer.xcodeproj/project.pbxproj index 3eb82f4..e2de109 100644 --- a/Prayer.xcodeproj/project.pbxproj +++ b/Prayer.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 2E06625B27243C9E001D8531 /* AudioMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E06625A27243C9E001D8531 /* AudioMode.swift */; }; 2E6C0B442646FD7A00A4D8E3 /* HandySwift in Frameworks */ = {isa = PBXBuildFile; productRef = 2E6C0B432646FD7A00A4D8E3 /* HandySwift */; }; 2E6C0B472646FD9200A4D8E3 /* HandyUIKit in Frameworks */ = {isa = PBXBuildFile; productRef = 2E6C0B462646FD9200A4D8E3 /* HandyUIKit */; }; 2E6C0B4A2646FDC800A4D8E3 /* Eureka in Frameworks */ = {isa = PBXBuildFile; productRef = 2E6C0B492646FDC800A4D8E3 /* Eureka */; }; @@ -29,9 +30,12 @@ 2E99770B2715A7B500F61BFE /* 100_The-Chargers.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2E99770D2715A7B500F61BFE /* 100_The-Chargers.txt */; }; 2E9977102715A7BD00F61BFE /* 101_The-Sudden-Calamity.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2E9977122715A7BD00F61BFE /* 101_The-Sudden-Calamity.txt */; }; 2E9977152715A7C500F61BFE /* 102_Greed-for-More-and-More.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2E9977172715A7C500F61BFE /* 102_Greed-for-More-and-More.txt */; }; + 2EA2CDF6271D2AC7009172C3 /* SpeechSynthesizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EA2CDF5271D2AC7009172C3 /* SpeechSynthesizer.swift */; }; 2EAF638A271AAC9D00145D5D /* RecitationPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF6389271AAC9D00145D5D /* RecitationPart.swift */; }; 2EAF6390271ADF3900145D5D /* NotificationToast in Frameworks */ = {isa = PBXBuildFile; productRef = 2EAF638F271ADF3900145D5D /* NotificationToast */; }; 2EAF6393271AE0B700145D5D /* UIViewControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EAF6392271AE0B700145D5D /* UIViewControllerExt.swift */; }; + 2ED15D71272DDCC3003BC370 /* AudioVolumeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ED15D70272DDCC3003BC370 /* AudioVolumeView.swift */; }; + 2ED15D74272DDF16003BC370 /* ViewRow in Frameworks */ = {isa = PBXBuildFile; productRef = 2ED15D73272DDF16003BC370 /* ViewRow */; }; A115CBF62222ED3800A7EB3A /* BartyCrouch.swift in Sources */ = {isa = PBXBuildFile; fileRef = A115CBF52222ED3800A7EB3A /* BartyCrouch.swift */; }; A115CBF82222EE0F00A7EB3A /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A115CBF72222EE0F00A7EB3A /* ErrorHandler.swift */; }; A115CBFA2222EE5700A7EB3A /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = A115CBF92222EE5700A7EB3A /* Assets.swift */; }; @@ -131,6 +135,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2E06625A27243C9E001D8531 /* AudioMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMode.swift; sourceTree = ""; }; 2E6C0B5E2647315600A4D8E3 /* .swift-format */ = {isa = PBXFileReference; lastKnownFileType = text; name = ".swift-format"; path = "../WhatNext-appleOS/.swift-format"; sourceTree = ""; }; 2E9976BD271585A500F61BFE /* Recitation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recitation.swift; sourceTree = ""; }; 2E9976DA2715A75400F61BFE /* en */ = {isa = PBXFileReference; lastKnownFileType = text; name = en; path = "en.lproj/090_The-Land.txt"; sourceTree = ""; }; @@ -172,11 +177,11 @@ 2E9977162715A7C500F61BFE /* en */ = {isa = PBXFileReference; lastKnownFileType = text; name = en; path = "en.lproj/102_Greed-for-More-and-More.txt"; sourceTree = ""; }; 2E9977182715A7C600F61BFE /* de */ = {isa = PBXFileReference; lastKnownFileType = text; name = de; path = "de.lproj/102_Greed-for-More-and-More.txt"; sourceTree = ""; }; 2E9977192715A7C700F61BFE /* tr */ = {isa = PBXFileReference; lastKnownFileType = text; name = tr; path = "tr.lproj/102_Greed-for-More-and-More.txt"; sourceTree = ""; }; + 2EA2CDF5271D2AC7009172C3 /* SpeechSynthesizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechSynthesizer.swift; sourceTree = ""; }; 2EAF6389271AAC9D00145D5D /* RecitationPart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecitationPart.swift; sourceTree = ""; }; 2EAF6392271AE0B700145D5D /* UIViewControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExt.swift; sourceTree = ""; }; + 2ED15D70272DDCC3003BC370 /* AudioVolumeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioVolumeView.swift; sourceTree = ""; }; A115CBEC2222E94C00A7EB3A /* .bartycrouch.toml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .bartycrouch.toml; sourceTree = ""; }; - A115CBEE2222E97100A7EB3A /* .periphery.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .periphery.yml; sourceTree = ""; }; - A115CBEF2222E97E00A7EB3A /* .projlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = .projlint.yml; sourceTree = ""; }; A115CBF22222EA4000A7EB3A /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; A115CBF42222ECDD00A7EB3A /* swiftgen.yml */ = {isa = PBXFileReference; lastKnownFileType = text; path = swiftgen.yml; sourceTree = ""; }; A115CBF52222ED3800A7EB3A /* BartyCrouch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BartyCrouch.swift; sourceTree = ""; }; @@ -323,6 +328,7 @@ 2E6C0B56264701D500A4D8E3 /* SwiftyBeaver in Frameworks */, 2E6C0B592647023900A4D8E3 /* SwiftyTimer in Frameworks */, 2E6C0B5C2647025800A4D8E3 /* SwiftyUserDefaults in Frameworks */, + 2ED15D74272DDF16003BC370 /* ViewRow in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -368,6 +374,7 @@ A1D1EFC31E393F080029C009 /* Countdown.swift */, A115CBF72222EE0F00A7EB3A /* ErrorHandler.swift */, A1370CD71E368DF300B55D9E /* Logger.swift */, + 2EA2CDF5271D2AC7009172C3 /* SpeechSynthesizer.swift */, 2EAF6391271AE0A400145D5D /* Extensions */, ); path = Globals; @@ -386,6 +393,7 @@ A11783EE21445DCE00B41A1A /* Models */ = { isa = PBXGroup; children = ( + 2E06625A27243C9E001D8531 /* AudioMode.swift */, A1A6A4651E3A819D00A909FF /* Position.swift */, A16484E21E243AAC008EC78C /* Prayer.swift */, A1D1EFC11E3937FC0029C009 /* PrayerState.swift */, @@ -435,6 +443,7 @@ isa = PBXGroup; children = ( A11830EE1E591FD900CBE087 /* Settings.storyboard */, + 2ED15D70272DDCC3003BC370 /* AudioVolumeView.swift */, A11830F81E59BDE200CBE087 /* FAQCollectionViewCell.swift */, A11830F01E5932BC00CBE087 /* FAQCollectionViewLayout.swift */, A11830EA1E591EF600CBE087 /* FAQViewController.swift */, @@ -468,8 +477,6 @@ children = ( A115CBEC2222E94C00A7EB3A /* .bartycrouch.toml */, A115CBF22222EA4000A7EB3A /* .gitignore */, - A115CBEE2222E97100A7EB3A /* .periphery.yml */, - A115CBEF2222E97E00A7EB3A /* .projlint.yml */, 2E6C0B5E2647315600A4D8E3 /* .swift-format */, A161E36D1E7EF8B1009C6602 /* fastlane */, A161E36B1E7EF826009C6602 /* Gemfile */, @@ -686,6 +693,7 @@ 2E6C0B582647023900A4D8E3 /* SwiftyTimer */, 2E6C0B5B2647025800A4D8E3 /* SwiftyUserDefaults */, 2EAF638F271ADF3900145D5D /* NotificationToast */, + 2ED15D73272DDF16003BC370 /* ViewRow */, ); productName = Prayer; productReference = A164848B1E243834008EC78C /* Prayer.app */; @@ -781,6 +789,7 @@ 2E6C0B572647023900A4D8E3 /* XCRemoteSwiftPackageReference "SwiftyTimer" */, 2E6C0B5A2647025800A4D8E3 /* XCRemoteSwiftPackageReference "SwiftyUserDefaults" */, 2EAF638E271ADF3900145D5D /* XCRemoteSwiftPackageReference "NotificationToast" */, + 2ED15D72272DDF16003BC370 /* XCRemoteSwiftPackageReference "ViewRow" */, ); productRefGroup = A164848C1E243834008EC78C /* Products */; projectDirPath = ""; @@ -902,7 +911,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which swift-format > /dev/null; then\n swift-format lint --recursive App/Sources Tests/Sources UITests/Sources\nelse\n echo \"warning: Swift-Format not installed, try `brew install swift-format`\"\nfi\n"; + shellScript = "export PATH=$PATH:/opt/homebrew/bin\n\nif which swift-format > /dev/null; then\n swift-format lint --recursive App/Sources Tests/Sources UITests/Sources\nelse\n echo \"warning: Swift-Format not installed, try `brew install swift-format`\"\nfi\n"; }; A1494BB81E2EBFB800286EBF /* BartyCrouch */ = { isa = PBXShellScriptBuildPhase; @@ -916,7 +925,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which bartycrouch > /dev/null; then\n bartycrouch update -x\n bartycrouch lint -x\nelse\n echo \"warning: BartyCrouch not installed, download it from https://github.com/Flinesoft/BartyCrouch\"\nfi\n"; + shellScript = "export PATH=$PATH:/opt/homebrew/bin\n\nif which bartycrouch > /dev/null; then\n bartycrouch update -x\n bartycrouch lint -x\nelse\n echo \"warning: BartyCrouch not installed, download it from https://github.com/Flinesoft/BartyCrouch\"\nfi\n"; }; A1494BCA1E2ED74700286EBF /* SwiftGen */ = { isa = PBXShellScriptBuildPhase; @@ -930,7 +939,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if which swiftgen > /dev/null; then\n swiftgen\nelse\n echo \"warning: SwiftGen not installed, download it from https://github.com/AliSoftware/SwiftGen\"\nfi\n"; + shellScript = "export PATH=$PATH:/opt/homebrew/bin\n\nif which swiftgen > /dev/null; then\n swiftgen\nelse\n echo \"warning: SwiftGen not installed, download it from https://github.com/AliSoftware/SwiftGen\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -941,7 +950,9 @@ files = ( A16484D41E243898008EC78C /* AppDelegate.swift in Sources */, A115CBFA2222EE5700A7EB3A /* Assets.swift in Sources */, + 2E06625B27243C9E001D8531 /* AudioMode.swift in Sources */, A1D1EFBE1E391C700029C009 /* AudioPlayer.swift in Sources */, + 2ED15D71272DDCC3003BC370 /* AudioVolumeView.swift in Sources */, A115CBF62222ED3800A7EB3A /* BartyCrouch.swift in Sources */, A1D1EFC41E393F080029C009 /* Countdown.swift in Sources */, A115CBF82222EE0F00A7EB3A /* ErrorHandler.swift in Sources */, @@ -963,6 +974,7 @@ A1370CDB1E368FA600B55D9E /* SettingsFlowController.swift in Sources */, A16484D51E243898008EC78C /* SettingsViewController.swift in Sources */, A1D1EFB71E381FD00029C009 /* SettingsViewModel.swift in Sources */, + 2EA2CDF6271D2AC7009172C3 /* SpeechSynthesizer.swift in Sources */, A1494BCF1E2ED79500286EBF /* Storyboards.swift in Sources */, A1494BCE1E2ED79500286EBF /* Strings.swift in Sources */, 2EAF6393271AE0B700145D5D /* UIViewControllerExt.swift in Sources */, @@ -1519,7 +1531,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 767S6EFMJ8; INFOPLIST_FILE = "$(SRCROOT)/App/SupportingFiles/Info.plist"; @@ -1527,7 +1539,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.3.0; PRODUCT_BUNDLE_IDENTIFIER = com.flinesoft.prayer; PRODUCT_MODULE_NAME = App; PRODUCT_NAME = Prayer; @@ -1540,7 +1552,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = 767S6EFMJ8; INFOPLIST_FILE = "$(SRCROOT)/App/SupportingFiles/Info.plist"; @@ -1548,7 +1560,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.3.0; PRODUCT_BUNDLE_IDENTIFIER = com.flinesoft.prayer; PRODUCT_MODULE_NAME = App; PRODUCT_NAME = Prayer; @@ -1749,6 +1761,14 @@ kind = branch; }; }; + 2ED15D72272DDF16003BC370 /* XCRemoteSwiftPackageReference "ViewRow" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/EurekaCommunity/ViewRow.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1797,6 +1817,11 @@ package = 2EAF638E271ADF3900145D5D /* XCRemoteSwiftPackageReference "NotificationToast" */; productName = NotificationToast; }; + 2ED15D73272DDF16003BC370 /* ViewRow */ = { + isa = XCSwiftPackageProductDependency; + package = 2ED15D72272DDF16003BC370 /* XCRemoteSwiftPackageReference "ViewRow" */; + productName = ViewRow; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = A16484831E243834008EC78C /* Project object */; diff --git a/Prayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Prayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 370df0d..1ca75d8 100644 --- a/Prayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Prayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -81,6 +81,15 @@ "revision": "f66bcd04088582c8fbb5cb8554d577e303bae396", "version": "5.3.0" } + }, + { + "package": "ViewRow", + "repositoryURL": "https://github.com/EurekaCommunity/ViewRow.git", + "state": { + "branch": null, + "revision": "3428a3b825a5641ae7fb65f3f787aba2b1b4dab9", + "version": "0.9.0" + } } ] }, diff --git a/README.md b/README.md index c9ed617..9931531 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ alt="Build Status"> - Version: 1.2.1 + Version: 1.3.0 - Swift: 5.4 + Swift: 5.5 Platforms: iOS @@ -49,12 +49,12 @@ You regularly pray your prayers in Arabic, but do **not really understand** what you're saying? -This app will help you make your **prayers in English** (or German or Turkish) to make them more meaningful to you. To do this, leave your device lying in front of you during the prayer and simply read the text from there. The app is purposely kept simple and reduced to the basic functions so that it does not contain distracting elements such as animations or advertising. +This app will help you make your **prayers in English** (or German or Turkish) to make them more meaningful to you. To do this, leave your device lying in front of you during the prayer and simply read the text from there. Or alternatively, configure the computer voice audio mode and listen to the text with your headphones during the prayer, while keeping your phone in your pocket. The app is purposely kept simple and reduced to the basic functions so that it does not contain distracting elements such as animations or advertising. ## Donation -BartyCrouch was brought to you by [Cihat Gündüz](https://github.com/Jeehut) in his free time. If you want to thank me and support the development of this project, please **make a small donation on [PayPal](https://paypal.me/Dschee/5EUR)**. In case you also like my other [open source contributions](https://github.com/Flinesoft) and [articles](https://medium.com/@Jeehut), please consider motivating me by **becoming a sponsor on [GitHub](https://github.com/sponsors/Jeehut)** or a **patron on [Patreon](https://www.patreon.com/Jeehut)**. +Prayer was brought to you by [Cihat Gündüz](https://github.com/Jeehut) in his free time. If you want to thank me and support the development of this project, please **make a small donation on [PayPal](https://paypal.me/Dschee/5EUR)**. In case you also like my other [open source contributions](https://github.com/Flinesoft) and [articles](https://medium.com/@Jeehut), please consider motivating me by **becoming a sponsor on [GitHub](https://github.com/sponsors/Jeehut)** or a **patron on [Patreon](https://www.patreon.com/Jeehut)**. Thank you very much for any donation, it really helps out a lot! 💯 diff --git a/Swiftgen-xcassets.stencil b/Swiftgen-xcassets.stencil index a845255..532dba8 100644 --- a/Swiftgen-xcassets.stencil +++ b/Swiftgen-xcassets.stencil @@ -15,7 +15,6 @@ {% set imageType %}UIImage{% endset %} #endif -// MARK: - Asset Catalogs {% macro enumBlock assets %} {% call casesBlock assets %} @@ -82,7 +81,6 @@ {% endif %} } -// MARK: - Implementation Details private final class BundleToken {} {% else %} diff --git a/Tests/Sources/Models/PrayerTests.swift b/Tests/Sources/Models/PrayerTests.swift index 56aa965..2f2cdbd 100644 --- a/Tests/Sources/Models/PrayerTests.swift +++ b/Tests/Sources/Models/PrayerTests.swift @@ -15,7 +15,7 @@ class PrayerTests: XCTestCase { // first rakah var expectedComponentNames = [ - "Takbīr", "Opening Supplication", "Ta'awwudh", "al-Fatiha (The Opening)", randomRecitation, "Takbīr", "Ruku", + "Takbīr", "Opening Supplication", "Ta'awwudh", "📖1: al-Fatiha (The Opening)", randomRecitation, "Takbīr", "Ruku", "Straightening Up", "Takbīr", "Sajdah", "Takbīr", "Takbīr", "Sajdah", "Takbīr", ] @@ -29,7 +29,7 @@ class PrayerTests: XCTestCase { // second rakah expectedComponentNames = [ - "al-Fatiha (The Opening)", randomRecitation, "Takbīr", "Ruku", "Straightening Up", "Takbīr", + "📖1: al-Fatiha (The Opening)", randomRecitation, "Takbīr", "Ruku", "Straightening Up", "Takbīr", "Sajdah", "Takbīr", "Takbīr", "Sajdah", "Takbīr", "Tashahhud", "Takbīr", ] @@ -42,7 +42,7 @@ class PrayerTests: XCTestCase { // third rakah expectedComponentNames = [ - "al-Fatiha (The Opening)", "Takbīr", "Ruku", "Straightening Up", "Takbīr", + "📖1: al-Fatiha (The Opening)", "Takbīr", "Ruku", "Straightening Up", "Takbīr", "Sajdah", "Takbīr", "Takbīr", "Sajdah", "Takbīr", ] @@ -55,7 +55,7 @@ class PrayerTests: XCTestCase { // fourth rakah expectedComponentNames = [ - "al-Fatiha (The Opening)", "Takbīr", "Ruku", "Straightening Up", "Takbīr", + "📖1: al-Fatiha (The Opening)", "Takbīr", "Ruku", "Straightening Up", "Takbīr", "Sajdah", "Takbīr", "Takbīr", "Sajdah", "Takbīr", "Tashahhud", "Salatul-'Ibrahimiyyah", "Rabbanagh", "Salâm", "Salâm", ] diff --git a/Tests/Sources/Models/RakahTests.swift b/Tests/Sources/Models/RakahTests.swift index 3aa83b2..dbf794c 100644 --- a/Tests/Sources/Models/RakahTests.swift +++ b/Tests/Sources/Models/RakahTests.swift @@ -17,7 +17,7 @@ class RakahTests: XCTestCase { let randomRecitation = "RR" let expectedComponentNames = [ - "Takbīr", "Opening Supplication", "Ta'awwudh", "al-Fatiha (The Opening)", randomRecitation, "Takbīr", "Ruku", + "Takbīr", "Opening Supplication", "Ta'awwudh", "📖1: al-Fatiha (The Opening)", randomRecitation, "Takbīr", "Ruku", "Straightening Up", "Takbīr", "Sajdah", "Takbīr", "Takbīr", "Sajdah", "Takbīr", ] @@ -40,7 +40,7 @@ class RakahTests: XCTestCase { let randomRecitation = "RR" let expectedComponentNames = [ - "al-Fatiha (The Opening)", "Takbīr", "Ruku", "Straightening Up", "Takbīr", + "📖1: al-Fatiha (The Opening)", "Takbīr", "Ruku", "Straightening Up", "Takbīr", "Sajdah", "Takbīr", "Takbīr", "Sajdah", "Takbīr", "Tashahhud", "Salatul-'Ibrahimiyyah", "Rabbanagh", "Salâm", "Salâm", ] diff --git a/UITests/Sources/AppStoreSnapshotUITests.swift b/UITests/Sources/AppStoreSnapshotUITests.swift index e9c6f96..aa4c9e4 100644 --- a/UITests/Sources/AppStoreSnapshotUITests.swift +++ b/UITests/Sources/AppStoreSnapshotUITests.swift @@ -6,20 +6,19 @@ import XCTest class AppStoreSnapshotUITests: XCTestCase { - // MARK: - Stored Instance Properties let app = XCUIApplication() - var uiMode: String = "light" + var uiMode: String = "1-light" - // MARK: - Test Methods override func setUp() { super.setUp() continueAfterFailure = false setupSnapshot(app) + app.launchArguments += ["UI_TESTS"] app.launch() - uiMode = app.launchArguments.contains("DARK_MODE") ? "dark" : "light" + uiMode = app.launchArguments.contains("DARK_MODE") ? "2-dark" : "1-light" XCUIDevice.shared.orientation = UIDevice.current.userInterfaceIdiom == .pad ? .landscapeLeft : .portrait } @@ -29,10 +28,36 @@ class AppStoreSnapshotUITests: XCTestCase { faqDoneButton.tap() } - snapshot("1-Settings-\(uiMode)") + if UIDevice.current.userInterfaceIdiom == .pad { + app.swipeUp() + snapshot("\(uiMode)-1-Settings") + } + else { + snapshot("\(uiMode)-1a-Settings-Top") + + app.swipeUp() + snapshot("\(uiMode)-1b-Settings-Bottom") + } + + if UIDevice.current.userInterfaceIdiom == .phone { + // turn off sounds during test + let audioModeText = localizedString(key: "SETTINGS.AUDIO_SPEED_SECTION.AUDIO_MODE.TITLE") + app.tables.staticTexts[audioModeText].tap() + + let audioModeNone = localizedString(key: "AUDIO_MODE.NONE") + app.tables.staticTexts[audioModeNone].tap() + } + else { + let audioModeNone = localizedString(key: "AUDIO_MODE.NONE") + app.tables.buttons[audioModeNone].tap() + } + + // turn the text speed all up + app.sliders.element(boundBy: 0).adjust(toNormalizedSliderPosition: 1) + app.sliders.element(boundBy: 1).adjust(toNormalizedSliderPosition: 1) // Wait until starting Opening Prayer - let startPrayerText = localizedString(key: "SETTINGS.START_BUTTON.TITLE") + let startPrayerText = "🕋 " + localizedString(key: "SETTINGS.START_BUTTON.TITLE") app.tables.staticTexts[startPrayerText].tap() let openingPrayerExpectation = expectation(description: "Going to Opening Prayer") @@ -45,8 +70,8 @@ class AppStoreSnapshotUITests: XCTestCase { openingPrayerExpectation.fulfill() } - waitForExpectations(timeout: 100, handler: nil) - snapshot("2-Opening-Prayer-\(uiMode)") + waitForExpectations(timeout: 60, handler: nil) + snapshot("\(uiMode)-2-Opening-Prayer") // Wait until going to Ruku let rukuExpectation = expectation(description: "Going to Ruku Screenshot") @@ -59,11 +84,10 @@ class AppStoreSnapshotUITests: XCTestCase { rukuExpectation.fulfill() } - waitForExpectations(timeout: 100, handler: nil) - snapshot("3-Ruku-\(uiMode)") + waitForExpectations(timeout: 60, handler: nil) + snapshot("\(uiMode)-3-Ruku") } - // MARK: - Helper Methods private func localizedString(key: String) -> String { let language = String(deviceLanguage.prefix(upTo: deviceLanguage.index(deviceLanguage.startIndex, offsetBy: 2))) let localizationBundle = Bundle( diff --git a/fastlane/Appfile b/fastlane/Appfile index c60ae90..1bdefbd 100644 --- a/fastlane/Appfile +++ b/fastlane/Appfile @@ -1,8 +1,5 @@ -app_identifier "com.flinesoft.prayer" # The bundle identifier of your app -apple_id ENV["APPLE_ID"] # Your Apple email address +app_identifier "com.flinesoft.prayer" +apple_id ENV["APPLE_ID"] -team_id ENV["DEV_PORTAL_TEAM_ID"] # Developer Portal Team ID -itc_team_id ENV["ITC_TEAM_ID"] # iTunes Connect Team ID - -# you can even provide different app identifiers, Apple IDs and team names per lane: -# More information: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Appfile.md +team_id ENV["DEV_PORTAL_TEAM_ID"] +itc_team_id ENV["ITC_TEAM_ID"] diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 4523f73..e89ead2 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -1,74 +1,14 @@ -# Customise this file, documentation can be found here: -# https://github.com/fastlane/fastlane/tree/master/fastlane/docs -# All available actions: https://docs.fastlane.tools/actions -# can also be listed using the `fastlane actions` command - -# Change the syntax highlighting to Ruby -# All lines starting with a # are ignored when running `fastlane` - -# If you want to automatically update fastlane if a new version is available: -# update_fastlane - -# This is the minimum version number required. -# Update this, if you use features of a newer version fastlane_version "2.196.0" default_platform :ios platform :ios do - before_all do - # ENV["SLACK_URL"] = "https://hooks.slack.com/services/..." - - # carthage - end - - desc "Runs all the tests" - lane :test do - scan - end - - desc "Submit a new Beta Build to Apple TestFlight" - desc "This will also make sure the profile is up to date" - lane :beta do - # match(type: "appstore") # more information: https://codesigning.guide - gym # Build your app - more options available - pilot - - # sh "your_script.sh" - # You can also use other beta testing services here (run `fastlane actions`) - end - desc "Deploy a new version to the App Store" lane :release do match(type: "appstore") snapshot(localize_simulator: true, dark_mode: true, launch_arguments: "DARK_MODE", clear_previous_screenshots: true) snapshot(localize_simulator: true, dark_mode: false) - # frameit(white: true) - gym(include_bitcode: true) # Build your app - more options available + gym(include_bitcode: true) deliver(force: true) end - - # You can define as many lanes as you want - - after_all do |lane| - # This block is called, only if the executed lane was successful - - # slack( - # message: "Successfully deployed new App Update." - # ) - end - - error do |lane, exception| - # slack( - # message: exception.message, - # success: false - # ) - end end - - -# More information about multiple platforms in fastlane: https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Platforms.md -# All available actions: https://docs.fastlane.tools/actions - -# fastlane reports which actions are used -# No personal data is recorded. Learn more at https://github.com/fastlane/enhancer diff --git a/fastlane/README.md b/fastlane/README.md index f72c97e..f2f7384 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -16,18 +16,6 @@ or alternatively using `brew install fastlane` # Available Actions ## iOS -### ios test -``` -fastlane ios test -``` -Runs all the tests -### ios beta -``` -fastlane ios beta -``` -Submit a new Beta Build to Apple TestFlight - -This will also make sure the profile is up to date ### ios release ``` fastlane ios release diff --git a/fastlane/Snapfile b/fastlane/Snapfile index a6d6829..e023978 100644 --- a/fastlane/Snapfile +++ b/fastlane/Snapfile @@ -1,5 +1,3 @@ -# Uncomment the lines below you want to change by removing the # in the beginning - # A list of devices you want to take the screenshots from devices([ "iPhone 8 Plus", diff --git a/fastlane/metadata/de-DE/description.txt b/fastlane/metadata/de-DE/description.txt index 9e1844d..a32b928 100644 --- a/fastlane/metadata/de-DE/description.txt +++ b/fastlane/metadata/de-DE/description.txt @@ -1,8 +1,8 @@ Du betest regelmäßig deine Gebete auf Arabisch, verstehst aber nicht wirklich, was du dabei sagst? -Diese App hilft dir, deine Gebete auf Deutsch (oder Türkisch bzw. Englisch) zu machen, damit sie für dich wieder an Bedeutung gewinnen. Hierzu lässt du dein Gerät während des Gebetes vor dir liegen und liest den Text einfach mit. Die App ist absichtlich schlicht gehalten und auf die Grundfunktionen reduziert, damit sie keine ablenkenden Elemente wie Animationen oder Werbung enthält. Werbung ist übrigens sowieso kein Thema, da der gesamte Code mit einer Open Source-Lizenz auf GitHub veröffentlicht ist (unter https://github.com/Flinesoft/Prayer). +Diese App hilft dir, deine Gebete auf Deutsch (oder Türkisch bzw. Englisch) zu machen, damit sie für dich wieder an Bedeutung gewinnen. Hierzu lässt du dein Gerät während des Gebetes vor dir liegen und liest den Text einfach mit. Alternativ kannst du die Computer-Vorlese-Stimme einstellen und den Text während des Gebets über deine Kopfhörer hören, während sich dein Telefon in der Hosentasche befindet. Die App ist absichtlich schlicht gehalten und auf die Grundfunktionen reduziert, damit sie keine ablenkenden Elemente wie Animationen oder Werbung enthält. Werbung ist übrigens sowieso kein Thema, da der gesamte Code mit einer Open Source-Lizenz auf GitHub veröffentlicht ist (unter https://github.com/Flinesoft/Prayer). -Wir empfehlen, dass du beim Beten mit dieser App (sofern möglich) den Lautlos-Modus deines Gerätes ausschaltest, da die App je einen kurzen Ton bei Auf- und Abwärtsbewegungen abspielt. Dass du den Ablauf eines Gebets bereits kennst, wird übrigens voraus gesetzt. Für Gebets-Neulinge ist diese App daher nicht geeignet. +Diese App setzt voraus, dass du den Ablauf eines Gebets bereits kennst. Für Gebets-Neulinge ist diese App daher nicht geeignet. Wir hoffen, dass du diese App nützlich findest. Wenn sie dir gefällt, freuen wir uns über eine entsprechende Bewertung im App Store. Für Fragen und Anregungen haben wir ein Forum eingerichtet: links.flinesoft.com/forum/prayer Dort findest du auch unsere Pläne für die Zukunft und kannst dich an der Diskussion beteiligen. diff --git a/fastlane/metadata/de-DE/release_notes.txt b/fastlane/metadata/de-DE/release_notes.txt index 058247b..50bc551 100644 --- a/fastlane/metadata/de-DE/release_notes.txt +++ b/fastlane/metadata/de-DE/release_notes.txt @@ -1,2 +1,12 @@ +NEU: +- Fügt einen neuen Audiomodus hinzu, bei dem eine Computerstimme den vollständigen Text des Gebets laut vorliest. +- Stellt sicher, dass der neue Audiomodus für gesprochenen Text weiter abgespielt wird, wenn das Gerät gesperrt ist oder die App im Hintergrund läuft. +- Zeigt das aktuell verbundene Audiogerät an und fügt eine Schaltfläche zum Wechseln des Geräts direkt in der App hinzu. +- Fügt detaillierte Beschreibungen unter jedem Einstellungsabschnitt hinzu, um zusätzlichen Kontext zu liefern. + +GEÄNDERT: +- Der Ton beim Positionswechsel ignoriert jetzt die Einstellung des Stummschalters des Systems. Künftig den Audiomodus stattdessen auf "stumm" stellen. +- Alle Einstellungseinträge haben jetzt ein Icon, um schneller erkannt zu werden und den Einstellungsbildschirm schöner aussehen zu lassen. + BEHOBEN: -- Einen Fehler mit der Hintergrundfarbe der oberen Leiste in der Gebetsansicht behoben. +- Die grüne Akzentfarbe im dunklen Modus wurde heller gestaltet, damit der Text auf den Schaltflächen besser lesbar ist. diff --git a/fastlane/metadata/en-US/description.txt b/fastlane/metadata/en-US/description.txt index dfd87d0..af9dfa8 100644 --- a/fastlane/metadata/en-US/description.txt +++ b/fastlane/metadata/en-US/description.txt @@ -1,8 +1,8 @@ You regularly pray your prayers in Arabic, but do not really understand what you're saying? -This app will help you make your prayers in English (or German or Turkish) to make them more meaningful to you. To do this, leave your device lying in front of you during the prayer and simply read the text from there. The app is purposely kept simple and reduced to the basic functions so that it does not contain distracting elements such as animations or advertising. Advertising is not an issue anyway, since the whole code is published with an open source license on GitHub (at https://github.com/Flinesoft/Prayer). +This app will help you make your prayers in English (or German or Turkish) to make them more meaningful to you. To do this, leave your device lying in front of you during the prayer and simply read the text from there. Or alternatively, configure the computer voice audio mode and listen to the text with your headphones during the prayer, while keeping your phone in your pocket. The app is purposely kept simple and reduced to the basic functions so that it does not contain distracting elements such as animations or advertising. Advertising is not an issue anyway, since the whole code is published with an open source license on GitHub (at https://github.com/Flinesoft/Prayer). -We recommend that you turn off silent mode when using this app (if possible), as the app plays a short tone for up and down movements. The app expects that you already know how a prayer is done. Therefore this app is not suitable for newcomers to prayers. +The app expects that you already know how a prayer is done. Therefore this app is not suitable for newcomers to prayers. We hope you find this app useful. If you like it, we would love to receive a rating in the App Store. For questions and suggestions, we have set up a forum: links.flinesoft.com/forum/prayer There, you will also find our plans for the future and you can even participate in the discussion. diff --git a/fastlane/metadata/en-US/release_notes.txt b/fastlane/metadata/en-US/release_notes.txt index 0785d59..1d354b0 100644 --- a/fastlane/metadata/en-US/release_notes.txt +++ b/fastlane/metadata/en-US/release_notes.txt @@ -1,2 +1,12 @@ -BEHOBEN: -- Fixed an issue with the background color of the top bar in the prayer view. +NEW: +- Adds a new audio mode where a computer voice reads out loud the full text of the prayer. +- Ensures the new spoken text audio mode continues to play when device is locked or app is in background. +- Shows the currently connected audio device and adds a button to switch the device right within the app. +- Adds detailed descriptions below each settings section to give some additional context. + +CHANGED: +- Position change sound now ignores the systems Mute switch setting. Set audio mode to `muted` instead to pray without any sounds. +- All settings entries now have an icon for faster recognition and to make the settings screen look nicer. + +FIXED: +- Adjusted green accent color in dark mode to be brighter for more legible text on buttons. diff --git a/fastlane/metadata/tr/description.txt b/fastlane/metadata/tr/description.txt index 4d03c4e..ca29263 100644 --- a/fastlane/metadata/tr/description.txt +++ b/fastlane/metadata/tr/description.txt @@ -1,8 +1,8 @@ Günde beş vakit namaz kılıyor da Arapça okuduğun için ne dediğini mi anlamıyorsun? -Bu uygulama Türkçe (veya Almanca yada İngilizce) namaz kılmanıza yardımcı olacaktır ki namazlarınızın anlamı olsun. Bunu yapmak için, namazda önünüzde cihazınızı bırakın ve oradan metni okuyarak namazınızı kılın. Uygulamanın kendisi bilerek basit tutulmuştur kı animasyon yada reklam gibi rahatsız edici seyler içermesin. Bu arada bütün kodu GitHub üzerinde bir açık kaynak lisansı ile yayınlanmış durumda (https://github.com/Flinesoft/Prayer) - böylece reklam sorunu hiç çıkmayacaktır. +Bu uygulama Türkçe (veya Almanca yada İngilizce) namaz kılmanıza yardımcı olacaktır ki namazlarınızın anlamı olsun. Bunu yapmak için, namazda önünüzde cihazınızı bırakın ve oradan metni okuyarak namazınızı kılın. Alternatif olarak, namaz esnasında telefonunuz cebinizdeyken bilgisayarın sesli okuma sesini ayarlayabilir ve metni kulaklıklarınızdan dinleyebilirsiniz. Uygulamanın kendisi bilerek basit tutulmuştur kı animasyon yada reklam gibi rahatsız edici seyler içermesin. Bu arada bütün kodu GitHub üzerinde bir açık kaynak lisansı ile yayınlanmış durumda (https://github.com/Flinesoft/Prayer) - böylece reklam sorunu hiç çıkmayacaktır. -Uygulama yukarı ve aşağı hareketler için kısa bir ses çıkardığı için bu uygulamayı kullanırken (mümkünse) sessiz modu kapatmanızı öneririz. Uygulama, bir duanın nasıl yapıldığını zaten bilmenizi bekler. Bu nedenle bu uygulama namaza yeni başlayanlar için uygun değildir. +Uygulama, bir duanın nasıl yapıldığını zaten bilmenizi bekler. Bu nedenle bu uygulama namaza yeni başlayanlar için uygun değildir. Biz bu uygulamanın yararlı bulacağını umuyoruz. Eğer sevdığseniz, bize App Store'da bir rating yaparsan seviniriz. links. Soru ve önerileriniz için, biz bir forum kurduk: links.flinesoft.com/forum/prayer Orada, bu uygulamanın geleceği için planımızı da bulacaksınız ve hatta tartışmaya katılabilirsiniz. diff --git a/fastlane/metadata/tr/release_notes.txt b/fastlane/metadata/tr/release_notes.txt index c4e2a2a..e743b3c 100644 --- a/fastlane/metadata/tr/release_notes.txt +++ b/fastlane/metadata/tr/release_notes.txt @@ -1,2 +1,12 @@ +YENİ: +- Bir bilgisayar sesinin duanın tam metnini yüksek sesle okuduğu yeni bir ses modu ekler. +- Cihaz kilitliyken veya uygulama arka plandayken yeni sözlü metin ses modunun oynamaya devam etmesini sağlar. +- Halihazırda bağlı olan ses cihazını gösterir ve cihazı doğrudan uygulama içinde değiştirmek için bir düğme ekler. +- Ek bağlam sağlamak için her ayar bölümünün altına ayrıntılı açıklamalar ekler. + +DEĞİŞEN: +- Konum değiştirme sesi artık sistemin Sessiz anahtarı ayarını yok sayar. Sessiz dua etmek için ses modunu "sessiz" olarak ayarlayın. +- Tüm ayar girişlerinde artık daha hızlı tanıma ve ayarlar ekranının daha güzel görünmesi için bir simge var. + DÜZELTİLEN: -- Dua görünümünde üst çubuğun arka plan rengiyle ilgili bir sorun düzeltildi. +- Düğmelerde daha okunaklı metinler için koyu modda yeşil vurgu rengi daha parlak olacak şekilde ayarlandı.