/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 5 End-User License Agreement and JUCE 5 Privacy Policy (both updated and effective as of the 27th April 2017). End User License Agreement: www.juce.com/juce-5-licence Privacy Policy: www.juce.com/juce-5-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #include "../JuceLibraryCode/JuceHeader.h" #include "VoicePurchases.h" //============================================================================== class InAppPurchaseApplication : public JUCEApplication, private AsyncUpdater { public: //============================================================================== InAppPurchaseApplication() : voicePurchases (*this) {} //============================================================================== const String getApplicationName() override { return ProjectInfo::projectName; } const String getApplicationVersion() override { return ProjectInfo::versionString; } bool moreThanOneInstanceAllowed() override { return false; } //============================================================================== void initialise (const String&) override { Desktop::getInstance().getDefaultLookAndFeel().setUsingNativeAlertWindows (true); dm.addAudioCallback (&player); dm.initialiseWithDefaultDevices (0, 2); mainWindow = new MainWindow; } void shutdown() override { mainWindow = nullptr; dm.closeAudioDevice(); dm.removeAudioCallback (&player); } static InAppPurchaseApplication* getInstance() { return static_cast (JUCEApplication::getInstance()); } //============================================================================== SoundPlayer& getPlayer() { return player; } VoicePurchases& getPurchases() { return voicePurchases; } //============================================================================== class MainContentComponent : public Component, private Button::Listener { public: MainContentComponent() { setOpaque (true); phraseListBox.setRowHeight (33); phraseListBox.selectRow (0); phraseListBox.updateContent(); voiceListBox.setRowHeight (66); voiceListBox.selectRow (0); voiceListBox.updateContent(); voiceListBox.getViewport()->setScrollOnDragEnabled (true); addAndMakeVisible (phraseLabel); addAndMakeVisible (phraseListBox); addAndMakeVisible (playStopButton); addAndMakeVisible (voiceLabel); addAndMakeVisible (voiceListBox); playStopButton.addListener (this); #if JUCE_ANDROID || JUCE_IOS auto screenBounds = Desktop::getInstance().getDisplays().getMainDisplay().userArea; setSize (screenBounds.getWidth(), screenBounds.getHeight()); #else setSize (800, 600); #endif } void updateDisplay() { voiceListBox.updateContent(); voiceListBox.repaint(); } private: //============================================================================== class PhraseModel : public ListBoxModel { public: PhraseModel() {} int getNumRows() override { return phrases.size(); } void paintListBoxItem (int row, Graphics& g, int w, int h, bool isSelected) override { Rectangle r (0, 0, w, h); auto& lf = Desktop::getInstance().getDefaultLookAndFeel(); g.setColour (lf.findColour (isSelected ? TextEditor::highlightColourId : ListBox::backgroundColourId)); g.fillRect (r); g.setColour (lf.findColour (ListBox::textColourId)); g.setFont (18); String phrase = (isPositiveAndBelow (row, phrases.size()) ? phrases[row] : String{}); g.drawText (phrase, 10, 0, w, h, Justification::centredLeft); } private: static StringArray phrases; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PhraseModel) }; //============================================================================== class VoiceModel : public ListBoxModel { public: //============================================================================== class VoiceRow : public Component, private Button::Listener, private Timer { public: VoiceRow () : voices (getInstance()->getPurchases().getVoiceNames()) { addAndMakeVisible (nameLabel); addAndMakeVisible (purchaseButton); addAndMakeVisible (priceLabel); purchaseButton.addListener (this); setSize (600, 33); } void paint (Graphics& g) override { auto r = getLocalBounds().reduced (4); { auto voiceIconBounds = r.removeFromLeft (r.getHeight()); g.setColour (Colours::black); g.drawRect (voiceIconBounds); voiceIconBounds.reduce (1, 1); g.setColour (hasBeenPurchased ? Colours::white : Colours::grey); g.fillRect (voiceIconBounds); g.drawImage (avatar, voiceIconBounds.toFloat()); if (! hasBeenPurchased) { g.setColour (Colours::white.withAlpha (0.8f)); g.fillRect (voiceIconBounds); if (purchaseInProgress) getLookAndFeel().drawSpinningWaitAnimation (g, Colours::darkgrey, voiceIconBounds.getX(), voiceIconBounds.getY(), voiceIconBounds.getWidth(), voiceIconBounds.getHeight()); } } } void resized() override { auto r = getLocalBounds().reduced (4 + 8, 4); auto h = r.getHeight(); auto w = static_cast (h * 1.5); r.removeFromLeft (h); purchaseButton.setBounds (r.removeFromRight (w).withSizeKeepingCentre (w, h / 2)); nameLabel.setBounds (r.removeFromTop (18)); priceLabel.setBounds (r.removeFromTop (18)); } void update (int rowNumber, bool rowIsSelected) { isSelected = rowIsSelected; rowSelected = rowNumber; if (isPositiveAndBelow (rowNumber, voices.size())) { auto imageResourceName = voices[rowNumber] + "_png"; nameLabel.setText (voices[rowNumber], NotificationType::dontSendNotification); auto purchase = getInstance()->getPurchases().getPurchase (rowNumber); hasBeenPurchased = purchase.isPurchased; purchaseInProgress = purchase.purchaseInProgress; if (purchaseInProgress) startTimer (1000 / 50); else stopTimer(); nameLabel.setFont (Font (16).withStyle (Font::bold | (hasBeenPurchased ? 0 : Font::italic))); nameLabel.setColour (Label::textColourId, hasBeenPurchased ? Colours::white : Colours::grey); priceLabel.setFont (Font (10).withStyle (purchase.priceIsKnown ? 0 : Font::italic)); priceLabel.setColour (Label::textColourId, hasBeenPurchased ? Colours::white : Colours::grey); priceLabel.setText (purchase.purchasePrice, NotificationType::dontSendNotification); if (rowNumber == 0) { purchaseButton.setButtonText ("Internal"); purchaseButton.setEnabled (false); } else { purchaseButton.setButtonText (hasBeenPurchased ? "Purchased" : "Purchase"); purchaseButton.setEnabled (! hasBeenPurchased && purchase.priceIsKnown); } setInterceptsMouseClicks (! hasBeenPurchased, ! hasBeenPurchased); int rawSize; if (auto* rawData = BinaryData::getNamedResource (imageResourceName.toRawUTF8(), rawSize)) { MemoryInputStream imageData (rawData, static_cast (rawSize), false); avatar = PNGImageFormat().decodeImage (imageData); } } } private: //============================================================================== void buttonClicked (Button*) override { if (rowSelected >= 0) { if (! hasBeenPurchased) { getInstance()->getPurchases().purchaseVoice (rowSelected); purchaseInProgress = true; startTimer (1000 / 50); } } } void timerCallback() override { repaint(); } //============================================================================== bool isSelected = false, hasBeenPurchased = false, purchaseInProgress = false; int rowSelected = -1; Image avatar; StringArray voices; Label nameLabel, priceLabel; TextButton purchaseButton {"Purchase"}; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VoiceRow) }; //============================================================================== VoiceModel() : voiceProducts (getInstance()->getPurchases().getVoiceNames()) {} int getNumRows() override { return voiceProducts.size(); } Component* refreshComponentForRow (int row, bool selected, Component* existing) override { if (isPositiveAndBelow (row, voiceProducts.size())) { if (existing == nullptr) existing = new VoiceRow; if (auto* voiceRow = dynamic_cast (existing)) voiceRow->update (row, selected); return existing; } return nullptr; } void paintListBoxItem (int, Graphics& g, int w, int h, bool isSelected) override { auto r = Rectangle (0, 0, w, h).reduced (4); auto& lf = Desktop::getInstance().getDefaultLookAndFeel(); g.setColour (lf.findColour (isSelected ? TextEditor::highlightColourId : ListBox::backgroundColourId)); g.fillRect (r); } private: StringArray voiceProducts; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VoiceModel) }; //============================================================================== void resized() override { auto r = getLocalBounds().reduced (20); { auto phraseArea = r.removeFromTop (r.getHeight() / 2); phraseLabel.setBounds (phraseArea.removeFromTop (36).reduced (0, 10)); playStopButton.setBounds (phraseArea.removeFromBottom (50).reduced (0, 10)); phraseListBox.setBounds (phraseArea); } { auto voiceArea = r; voiceLabel.setBounds (voiceArea.removeFromTop (36).reduced (0, 10)); voiceListBox.setBounds (voiceArea); } } void paint (Graphics& g) override { g.fillAll (Desktop::getInstance().getDefaultLookAndFeel() .findColour (ResizableWindow::backgroundColourId)); } //============================================================================== void buttonClicked (Button*) override { MemoryOutputStream resourceName; auto idx = voiceListBox.getSelectedRow(); if (isPositiveAndBelow (idx, soundNames.size())) { resourceName << soundNames[idx] << phraseListBox.getSelectedRow() << "_ogg"; int numBytes; if (auto* data = BinaryData::getNamedResource (resourceName.toString().toRawUTF8(), numBytes)) getInstance()->getPlayer().play (data, static_cast (numBytes)); } } //============================================================================== StringArray soundNames = getInstance()->getPurchases().getVoiceNames(); PhraseModel phraseModel; Label phraseLabel {"phraseLabel", NEEDS_TRANS ("Phrases:")}; ListBox phraseListBox { "phraseListBox", &phraseModel }; TextButton playStopButton {"Play"}; VoiceModel voiceModel; Label voiceLabel {"voiceLabel", NEEDS_TRANS ("Voices:")}; ListBox voiceListBox { "voiceListBox", &voiceModel }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent) }; //============================================================================== class MainWindow : public DocumentWindow { public: MainWindow () : DocumentWindow (ProjectInfo::projectName, Desktop::getInstance().getDefaultLookAndFeel() .findColour (ResizableWindow::backgroundColourId), DocumentWindow::allButtons) { setUsingNativeTitleBar (true); setContentOwned (new MainContentComponent(), true); #if JUCE_ANDROID || JUCE_IOS setFullScreen (true); #else centreWithSize (getWidth(), getHeight()); #endif setVisible (true); } void closeButtonPressed() override { JUCEApplication::getInstance()->systemRequestedQuit(); } private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow) }; private: //============================================================================== void handleAsyncUpdate() override { if (auto* contentComponent = dynamic_cast (mainWindow->getContentComponent())) contentComponent->updateDisplay(); } //============================================================================== VoicePurchases voicePurchases; AudioDeviceManager dm; SoundPlayer player; ScopedPointer mainWindow; ScopedPointer alertWindow; }; StringArray InAppPurchaseApplication::MainContentComponent::PhraseModel::phrases {{"I love JUCE!", "The five dimensions of touch", "Make it fast!"}}; //============================================================================== // This macro generates the main() routine that launches the app. START_JUCE_APPLICATION (InAppPurchaseApplication)