WIP
This commit is contained in:
parent
a32757c518
commit
46e12db1fd
26
.idea/appInsightsSettings.xml
generated
Normal file
26
.idea/appInsightsSettings.xml
generated
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AppInsightsSettings">
|
||||
<option name="tabSettings">
|
||||
<map>
|
||||
<entry key="Firebase Crashlytics">
|
||||
<value>
|
||||
<InsightsFilterSettings>
|
||||
<option name="connection">
|
||||
<ConnectionSetting>
|
||||
<option name="appId" value="PLACEHOLDER" />
|
||||
<option name="mobileSdkAppId" value="" />
|
||||
<option name="projectId" value="" />
|
||||
<option name="projectNumber" value="" />
|
||||
</ConnectionSetting>
|
||||
</option>
|
||||
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||
<option name="visibilityType" value="ALL" />
|
||||
</InsightsFilterSettings>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/assetWizardSettings.xml
generated
6
.idea/assetWizardSettings.xml
generated
@ -328,7 +328,7 @@
|
||||
<PersistentState>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="url" value="file:/$USER_HOME$/AppData/Local/Android/Sdk/icons/material/materialicons/article/baseline_article_24.xml" />
|
||||
<entry key="url" value="file:/$USER_HOME$/AppData/Local/Android/Sdk/icons/material/materialicons/circle/baseline_circle_24.xml" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
@ -338,8 +338,8 @@
|
||||
</option>
|
||||
<option name="values">
|
||||
<map>
|
||||
<entry key="outputName" value="baseline_article_24" />
|
||||
<entry key="sourceFile" value="C:\Users\erike\Downloads\hashtag-solid.svg" />
|
||||
<entry key="outputName" value="baseline_circle_24" />
|
||||
<entry key="sourceFile" value="C:\Users\erike\OneDrive\Imágenes\logo-openfire.svg" />
|
||||
</map>
|
||||
</option>
|
||||
</PersistentState>
|
||||
|
||||
340
.idea/caches/deviceStreaming.xml
generated
Normal file
340
.idea/caches/deviceStreaming.xml
generated
Normal file
@ -0,0 +1,340 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceStreaming">
|
||||
<option name="deviceSelectionList">
|
||||
<list>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="27" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="F01L" />
|
||||
<option name="id" value="F01L" />
|
||||
<option name="manufacturer" value="FUJITSU" />
|
||||
<option name="name" value="F-01L" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1280" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="OPPO" />
|
||||
<option name="codename" value="OP573DL1" />
|
||||
<option name="id" value="OP573DL1" />
|
||||
<option name="manufacturer" value="OPPO" />
|
||||
<option name="name" value="CPH2557" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="28" />
|
||||
<option name="brand" value="DOCOMO" />
|
||||
<option name="codename" value="SH-01L" />
|
||||
<option name="id" value="SH-01L" />
|
||||
<option name="manufacturer" value="SHARP" />
|
||||
<option name="name" value="AQUOS sense2 SH-01L" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="Lenovo" />
|
||||
<option name="codename" value="TB370FU" />
|
||||
<option name="id" value="TB370FU" />
|
||||
<option name="manufacturer" value="Lenovo" />
|
||||
<option name="name" value="Tab P12" />
|
||||
<option name="screenDensity" value="340" />
|
||||
<option name="screenX" value="1840" />
|
||||
<option name="screenY" value="2944" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="a51" />
|
||||
<option name="id" value="a51" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy A51" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="akita" />
|
||||
<option name="id" value="akita" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="b0q" />
|
||||
<option name="id" value="b0q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S22 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="32" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="bluejay" />
|
||||
<option name="id" value="bluejay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="caiman" />
|
||||
<option name="id" value="caiman" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="960" />
|
||||
<option name="screenY" value="2142" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="comet" />
|
||||
<option name="id" value="comet" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro Fold" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="2076" />
|
||||
<option name="screenY" value="2152" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="29" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="crownqlteue" />
|
||||
<option name="id" value="crownqlteue" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Note9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2220" />
|
||||
<option name="screenY" value="1080" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="dm3q" />
|
||||
<option name="id" value="dm3q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S23 Ultra" />
|
||||
<option name="screenDensity" value="600" />
|
||||
<option name="screenX" value="1440" />
|
||||
<option name="screenY" value="3088" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="e1q" />
|
||||
<option name="id" value="e1q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy S24" />
|
||||
<option name="screenDensity" value="480" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix" />
|
||||
<option name="id" value="felix" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="felix_camera" />
|
||||
<option name="id" value="felix_camera" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="2208" />
|
||||
<option name="screenY" value="1840" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="gts8uwifi" />
|
||||
<option name="id" value="gts8uwifi" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Tab S8 Ultra" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1848" />
|
||||
<option name="screenY" value="2960" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="husky" />
|
||||
<option name="id" value="husky" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8 Pro" />
|
||||
<option name="screenDensity" value="390" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="motorola" />
|
||||
<option name="codename" value="java" />
|
||||
<option name="id" value="java" />
|
||||
<option name="manufacturer" value="Motorola" />
|
||||
<option name="name" value="G20" />
|
||||
<option name="screenDensity" value="280" />
|
||||
<option name="screenX" value="720" />
|
||||
<option name="screenY" value="1600" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="komodo" />
|
||||
<option name="id" value="komodo" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9 Pro XL" />
|
||||
<option name="screenDensity" value="360" />
|
||||
<option name="screenX" value="1008" />
|
||||
<option name="screenY" value="2244" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="lynx" />
|
||||
<option name="id" value="lynx" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7a" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="31" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="oriole" />
|
||||
<option name="id" value="oriole" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="panther" />
|
||||
<option name="id" value="panther" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 7" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q5q" />
|
||||
<option name="id" value="q5q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold5" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1812" />
|
||||
<option name="screenY" value="2176" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="samsung" />
|
||||
<option name="codename" value="q6q" />
|
||||
<option name="id" value="q6q" />
|
||||
<option name="manufacturer" value="Samsung" />
|
||||
<option name="name" value="Galaxy Z Fold6" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1856" />
|
||||
<option name="screenY" value="2160" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="r11" />
|
||||
<option name="id" value="r11" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Watch" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="384" />
|
||||
<option name="screenY" value="384" />
|
||||
<option name="type" value="WEAR_OS" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="30" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="redfin" />
|
||||
<option name="id" value="redfin" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 5" />
|
||||
<option name="screenDensity" value="440" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2340" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="shiba" />
|
||||
<option name="id" value="shiba" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 8" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2400" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="33" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tangorpro" />
|
||||
<option name="id" value="tangorpro" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel Tablet" />
|
||||
<option name="screenDensity" value="320" />
|
||||
<option name="screenX" value="1600" />
|
||||
<option name="screenY" value="2560" />
|
||||
</PersistentDeviceSelectionData>
|
||||
<PersistentDeviceSelectionData>
|
||||
<option name="api" value="34" />
|
||||
<option name="brand" value="google" />
|
||||
<option name="codename" value="tokay" />
|
||||
<option name="id" value="tokay" />
|
||||
<option name="manufacturer" value="Google" />
|
||||
<option name="name" value="Pixel 9" />
|
||||
<option name="screenDensity" value="420" />
|
||||
<option name="screenX" value="1080" />
|
||||
<option name="screenY" value="2424" />
|
||||
</PersistentDeviceSelectionData>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
<bytecodeTargetLevel target="21" />
|
||||
</component>
|
||||
</project>
|
||||
23
.idea/deploymentTargetDropDown.xml
generated
Normal file
23
.idea/deploymentTargetDropDown.xml
generated
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<value>
|
||||
<entry key="app">
|
||||
<State>
|
||||
<targetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="C:\Users\erike\.android\avd\Pixel_7_API_34.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2024-04-25T06:26:20.392506800Z" />
|
||||
</State>
|
||||
</entry>
|
||||
</value>
|
||||
</component>
|
||||
</project>
|
||||
18
.idea/deploymentTargetSelector.xml
generated
Normal file
18
.idea/deploymentTargetSelector.xml
generated
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-05-05T02:26:13.673098400Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\erike\.android\avd\Pixel_7_API_34.avd" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/gradle.xml
generated
1
.idea/gradle.xml
generated
@ -4,6 +4,7 @@
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
|
||||
69
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
69
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,69 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
2
.idea/kotlinc.xml
generated
2
.idea/kotlinc.xml
generated
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.0" />
|
||||
<option name="version" value="2.1.0" />
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -1,6 +1,6 @@
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
98
.idea/navEditor.xml
generated
98
.idea/navEditor.xml
generated
@ -323,6 +323,75 @@
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="messenger_navigation.xml">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="contactsFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="-61" />
|
||||
<option name="y" value="-20" />
|
||||
</Point>
|
||||
</option>
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="action_contactsFragment_to_conversationFragment">
|
||||
<value>
|
||||
<LayoutPositions />
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="action_contactsFragment_to_registrationFragment">
|
||||
<value>
|
||||
<LayoutPositions />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="conversationFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="151" />
|
||||
<option name="y" value="-166" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="registrationFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="170" />
|
||||
<option name="y" value="142" />
|
||||
</Point>
|
||||
</option>
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="action_registrationFragment_to_contactsFragment">
|
||||
<value>
|
||||
<LayoutPositions />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="picture_viewer_navigation.xml">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
@ -384,6 +453,18 @@
|
||||
<LayoutPositions>
|
||||
<option name="myPositions">
|
||||
<map>
|
||||
<entry key="hashtagPostsFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="345" />
|
||||
<option name="y" value="45" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="postListingFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
@ -517,11 +598,28 @@
|
||||
<LayoutPositions />
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="action_mainFragment_to_qrCodeFragment">
|
||||
<value>
|
||||
<LayoutPositions />
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="qrCodeFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
<option name="myPosition">
|
||||
<Point>
|
||||
<option name="x" value="12" />
|
||||
<option name="y" value="12" />
|
||||
</Point>
|
||||
</option>
|
||||
</LayoutPositions>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="reportProfileFragment">
|
||||
<value>
|
||||
<LayoutPositions>
|
||||
|
||||
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@ -2,7 +2,6 @@ plugins {
|
||||
id 'com.android.application'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'com.google.dagger.hilt.android'
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.0'
|
||||
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
|
||||
@ -10,6 +9,8 @@ plugins {
|
||||
id 'com.google.gms.google-services'
|
||||
id 'com.google.firebase.crashlytics'
|
||||
id 'com.google.android.gms.oss-licenses-plugin'
|
||||
id 'org.jetbrains.kotlin.plugin.compose' version '2.1.0'
|
||||
id 'com.google.devtools.ksp'
|
||||
}
|
||||
|
||||
android {
|
||||
@ -23,10 +24,6 @@ android {
|
||||
compose true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.2"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.isolaatti"
|
||||
minSdk 24
|
||||
@ -54,7 +51,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.3'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
@ -73,8 +70,8 @@ dependencies {
|
||||
implementation "androidx.datastore:datastore-preferences:1.0.0"
|
||||
|
||||
// Hilt
|
||||
implementation "com.google.dagger:hilt-android:2.47"
|
||||
kapt "com.google.dagger:hilt-compiler:2.47"
|
||||
implementation "com.google.dagger:hilt-android:2.53.1"
|
||||
ksp "com.google.dagger:hilt-compiler:2.53.1"
|
||||
|
||||
|
||||
// Retrofit
|
||||
@ -120,11 +117,10 @@ dependencies {
|
||||
}
|
||||
|
||||
// Room Database
|
||||
def room_version = "2.5.2"
|
||||
def room_version = "2.6.1"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
annotationProcessor "androidx.room:room-compiler:$room_version"
|
||||
kapt("androidx.room:room-compiler:$room_version")
|
||||
implementation "androidx.room:room-ktx:2.5.2"
|
||||
ksp("androidx.room:room-compiler:$room_version")
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
|
||||
// Image viewer
|
||||
implementation 'com.github.MikeOrtiz:TouchImageView:3.5'
|
||||
@ -138,7 +134,7 @@ dependencies {
|
||||
implementation 'com.github.androidmads:QRGenerator:1.0.1'
|
||||
|
||||
// Firebase
|
||||
implementation(platform("com.google.firebase:firebase-bom:32.7.3"))
|
||||
implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
|
||||
implementation("com.google.firebase:firebase-crashlytics")
|
||||
implementation("com.google.firebase:firebase-analytics")
|
||||
implementation("com.google.firebase:firebase-messaging")
|
||||
@ -146,7 +142,7 @@ dependencies {
|
||||
// OSS screen
|
||||
implementation 'com.google.android.gms:play-services-oss-licenses:17.1.0'
|
||||
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.10.01')
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.12.01')
|
||||
implementation composeBom
|
||||
androidTestImplementation composeBom
|
||||
implementation 'androidx.compose.material3:material3'
|
||||
|
||||
@ -49,10 +49,7 @@
|
||||
<activity android:name=".images.picture_viewer.ui.PictureViewerActivity" android:theme="@style/Theme.Isolaatti"/>
|
||||
<activity android:name=".sign_up.ui.SignUpActivity" android:theme="@style/Theme.Isolaatti"/>
|
||||
<activity android:name=".images.image_maker.ui.ImageMakerActivity" android:theme="@style/Theme.Isolaatti"/>
|
||||
<activity android:name=".images.image_chooser.ui.ImageChooserActivity" android:theme="@style/Theme.Isolaatti"/>
|
||||
<activity android:name=".profile.ui.EditProfileActivity" android:theme="@style/Theme.Isolaatti" />
|
||||
<activity android:name=".audio.recorder.ui.AudioRecorderActivity" android:theme="@style/Theme.Isolaatti" />
|
||||
<activity android:name=".audio.audio_selector.ui.AudioSelectorActivity" android:theme="@style/Theme.Isolaatti" />
|
||||
<activity android:name=".posting.posts.ui.PostInfoActivity" android:theme="@style/Theme.Isolaatti"/>
|
||||
<activity android:name=".hashtags.ui.HashtagsPostsActivity" android:theme="@style/Theme.Isolaatti" />
|
||||
<provider
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
package com.isolaatti.audio
|
||||
|
||||
import androidx.media3.common.Player
|
||||
import com.isolaatti.audio.common.data.AudiosApi
|
||||
import com.isolaatti.audio.common.data.AudiosRepositoryImpl
|
||||
import com.isolaatti.audio.common.domain.AudiosRepository
|
||||
import com.isolaatti.audio.drafts.data.AudioDraftsRepositoryImpl
|
||||
import com.isolaatti.audio.drafts.data.AudiosDraftsDao
|
||||
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
||||
import com.isolaatti.connectivity.RetrofitClient
|
||||
import com.isolaatti.database.AppDatabase
|
||||
import com.isolaatti.settings.domain.UserIdSetting
|
||||
@ -24,17 +20,7 @@ class Module {
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideAudiosRepository(audiosApi: AudiosApi, audiosDraftsDao: AudiosDraftsDao, userIdSetting: UserIdSetting): AudiosRepository {
|
||||
return AudiosRepositoryImpl(audiosApi, audiosDraftsDao, userIdSetting)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideAudioDraftsDao(database: AppDatabase): AudiosDraftsDao {
|
||||
return database.audioDrafts()
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideAudioDraftsRepository(audiosDraftsDao: AudiosDraftsDao): AudioDraftsRepository {
|
||||
return AudioDraftsRepositoryImpl(audiosDraftsDao)
|
||||
fun provideAudiosRepository(audiosApi: AudiosApi, userIdSetting: UserIdSetting): AudiosRepository {
|
||||
return AudiosRepositoryImpl(audiosApi, userIdSetting)
|
||||
}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
package com.isolaatti.audio.audio_selector.presentation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.isolaatti.audio.audio_selector.ui.AudiosListSelectorFragment
|
||||
|
||||
class AudioSelectorFragmentAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
|
||||
override fun getItemCount(): Int = 2
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when(position) {
|
||||
0 -> AudiosListSelectorFragment.getInstance()
|
||||
1 -> AudiosListSelectorFragment.getInstanceForDrafts()
|
||||
else -> Fragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
package com.isolaatti.audio.audio_selector.presentation
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.AudiosRepository
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
||||
import com.isolaatti.auth.domain.AuthRepository
|
||||
import com.isolaatti.common.SortingEnum
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AudioSelectorViewModel @Inject constructor(
|
||||
private val audiosRepository: AudiosRepository,
|
||||
private val audioDraftsRepository: AudioDraftsRepository
|
||||
) : ViewModel() {
|
||||
val audios: MutableLiveData<List<Audio>> = MutableLiveData()
|
||||
val drafts: MutableLiveData<List<AudioDraft>> = MutableLiveData()
|
||||
val sorting: MutableLiveData<SortingEnum> = MutableLiveData()
|
||||
var squadId: String? = null
|
||||
val error: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData(null)
|
||||
val loading: MutableLiveData<Boolean> = MutableLiveData()
|
||||
fun setSorting(sort: SortingEnum) {
|
||||
sorting.value = sort
|
||||
}
|
||||
|
||||
fun getAudioDrafts() {
|
||||
viewModelScope.launch {
|
||||
audioDraftsRepository.getAudioDrafts().onEach {
|
||||
drafts.postValue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAudios() {
|
||||
if(squadId == null) {
|
||||
viewModelScope.launch {
|
||||
audiosRepository.getMyAudios(audios.value?.lastOrNull()?.id).onEach { resource ->
|
||||
when(resource) {
|
||||
is Resource.Error -> {
|
||||
loading.postValue(false)
|
||||
error.postValue(resource.errorType)
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
loading.postValue(true)
|
||||
}
|
||||
is Resource.Success -> {
|
||||
loading.postValue(false)
|
||||
audios.postValue(resource.data ?: listOf())
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,91 +0,0 @@
|
||||
package com.isolaatti.audio.audio_selector.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.audio_selector.presentation.AudioSelectorFragmentAdapter
|
||||
import com.isolaatti.audio.audio_selector.presentation.AudioSelectorViewModel
|
||||
import com.isolaatti.audio.recorder.ui.AudioRecorderContract
|
||||
import com.isolaatti.common.IsolaattiBaseActivity
|
||||
import com.isolaatti.common.SortingEnum
|
||||
import com.isolaatti.databinding.ActivityAudioSelectorBinding
|
||||
|
||||
class AudioSelectorActivity : IsolaattiBaseActivity() {
|
||||
companion object {
|
||||
const val EXTRA_ID = "id"
|
||||
const val EXTRA_FOR_SQUAD = "forSquad"
|
||||
const val OUT_EXTRA_PLAYABLE = "playable"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityAudioSelectorBinding
|
||||
private val viewModel: AudioSelectorViewModel by viewModels()
|
||||
|
||||
private val audioRecorderLauncher = registerForActivityResult(AudioRecorderContract()) {
|
||||
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityAudioSelectorBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.viewpager.adapter = AudioSelectorFragmentAdapter(this)
|
||||
|
||||
TabLayoutMediator(binding.tabLayout, binding.viewpager) {tab, position ->
|
||||
when(position) {
|
||||
0 -> tab.text = getString(R.string.audios)
|
||||
1 -> tab.text = getString(R.string.drafts)
|
||||
}
|
||||
}.attach()
|
||||
|
||||
binding.sort.setText(R.string.descending_by_name)
|
||||
|
||||
setupListeners()
|
||||
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
finish()
|
||||
}
|
||||
binding.newAudio.setOnClickListener {
|
||||
audioRecorderLauncher.launch(null)
|
||||
}
|
||||
|
||||
binding.sort.setOnClickListener {
|
||||
val popupMenu = PopupMenu(this, it)
|
||||
popupMenu.inflate(R.menu.audios_sort_menu)
|
||||
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
binding.sort.text = menuItem.title
|
||||
when(menuItem.itemId) {
|
||||
R.id.desc_by_name -> {
|
||||
viewModel.setSorting(SortingEnum.DescendingByName)
|
||||
true
|
||||
}
|
||||
R.id.asc_by_name -> {
|
||||
viewModel.setSorting(SortingEnum.AscendingByName)
|
||||
true
|
||||
}
|
||||
R.id.asc_by_creation_date -> {
|
||||
viewModel.setSorting(SortingEnum.AscendingByCreationDate)
|
||||
true
|
||||
}
|
||||
R.id.desc_by_creation_date -> {
|
||||
viewModel.setSorting(SortingEnum.DescendingByCreationDate)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
popupMenu.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
package com.isolaatti.audio.audio_selector.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import java.io.Serializable
|
||||
|
||||
/**
|
||||
* Contract to select audio. Use SelectorConfig class to specify the context to use to select audio.
|
||||
* Output: if user selected an audio or draft, it will be returned as result. Check type as convenient. Null if user cancelled
|
||||
*/
|
||||
class AudioSelectorContract : ActivityResultContract<AudioSelectorContract.SelectorConfig, Playable?>() {
|
||||
|
||||
/**
|
||||
* @param forSquad audios source will be a specified squad in the id param
|
||||
* @param id squad id, should be non null if forSquad is true
|
||||
*/
|
||||
data class SelectorConfig(val forSquad: Boolean, val id: String?): Serializable
|
||||
|
||||
override fun createIntent(context: Context, input: SelectorConfig): Intent {
|
||||
val intent = Intent(context, AudioSelectorActivity::class.java)
|
||||
intent.apply {
|
||||
putExtra(AudioSelectorActivity.EXTRA_FOR_SQUAD, input.forSquad)
|
||||
if(input.forSquad) {
|
||||
putExtra(AudioSelectorActivity.EXTRA_ID, input.id as String)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return intent
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Playable? {
|
||||
if(resultCode == Activity.RESULT_OK) {
|
||||
return intent?.extras?.getSerializable(AudioSelectorActivity.OUT_EXTRA_PLAYABLE) as? Playable
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
package com.isolaatti.audio.audio_selector.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.isolaatti.audio.audio_selector.presentation.AudioSelectorViewModel
|
||||
import com.isolaatti.audio.audios_list.presentation.AudiosAdapter
|
||||
import com.isolaatti.databinding.FragmentAudiosListSelectorBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AudiosListSelectorFragment : Fragment() {
|
||||
private lateinit var binding: FragmentAudiosListSelectorBinding
|
||||
private val viewModel: AudioSelectorViewModel by activityViewModels()
|
||||
|
||||
private var mode: Int = ARG_VAL_MODE_AUDIOS
|
||||
private var squadId: String? = null
|
||||
|
||||
private lateinit var adapter: AudiosAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.getInt(ARG_MODE)?.let { mode = it }
|
||||
arguments?.getString(ARG_SQUAD_ID)?.let { squadId = it }
|
||||
viewModel.squadId = squadId
|
||||
|
||||
when(mode) {
|
||||
ARG_VAL_MODE_AUDIOS -> {
|
||||
viewModel.getAudios()
|
||||
}
|
||||
ARG_VAL_MODE_DRAFTS -> {
|
||||
viewModel.getAudioDrafts()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentAudiosListSelectorBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
adapter = AudiosAdapter(
|
||||
onPlayClick = {
|
||||
|
||||
},
|
||||
onOptionsClick = {audio, button ->
|
||||
false
|
||||
}
|
||||
)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
when(mode) {
|
||||
ARG_VAL_MODE_AUDIOS -> {
|
||||
viewModel.audios.observe(viewLifecycleOwner) {
|
||||
adapter.setData(it)
|
||||
}
|
||||
}
|
||||
ARG_VAL_MODE_DRAFTS -> {
|
||||
viewModel.drafts.observe(viewLifecycleOwner) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_MODE = "mode"
|
||||
private const val ARG_SQUAD_ID = "squadId"
|
||||
private const val ARG_VAL_MODE_DRAFTS = 0
|
||||
private const val ARG_VAL_MODE_AUDIOS = 1
|
||||
fun getInstance(): AudiosListSelectorFragment {
|
||||
return AudiosListSelectorFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(ARG_MODE, ARG_VAL_MODE_AUDIOS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getInstanceForDrafts(): AudiosListSelectorFragment {
|
||||
return AudiosListSelectorFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(ARG_MODE, ARG_VAL_MODE_DRAFTS)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun getInstanceForSquad(squadId: String): AudiosListSelectorFragment {
|
||||
return AudiosListSelectorFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(ARG_SQUAD_ID, squadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,154 +0,0 @@
|
||||
package com.isolaatti.audio.audios_list.presentation
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import coil.load
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.databinding.AudioListItemBinding
|
||||
|
||||
class AudiosAdapter(
|
||||
private val onPlayClick: ((audio: Audio) -> Unit),
|
||||
private val onOptionsClick: ((audio: Audio, button: View) -> Boolean)
|
||||
) : RecyclerView.Adapter<AudiosAdapter.AudiosViewHolder>() {
|
||||
|
||||
enum class Payload {
|
||||
PlayStateChanged, IsLoadingChanged
|
||||
}
|
||||
|
||||
private var data: List<Audio> = listOf()
|
||||
private var currentPlaying: Audio? = null
|
||||
|
||||
fun setData(audios: List<Audio>) {
|
||||
data = audios
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setIsPlaying(isPlaying: Boolean, audio: Audio) {
|
||||
if(audio == currentPlaying) {
|
||||
val index = data.indexOf(audio)
|
||||
if(index == -1) return
|
||||
|
||||
data[index].isPlaying = isPlaying
|
||||
|
||||
notifyItemChanged(index, Payload.PlayStateChanged)
|
||||
return
|
||||
}
|
||||
|
||||
val prevIndex = data.indexOf(currentPlaying)
|
||||
|
||||
if(prevIndex != -1) {
|
||||
data[prevIndex].isPlaying = false
|
||||
notifyItemChanged(prevIndex, Payload.PlayStateChanged)
|
||||
}
|
||||
val newIndex = data.indexOf(audio)
|
||||
if(newIndex != -1) {
|
||||
data[newIndex].isPlaying = isPlaying
|
||||
notifyItemChanged(newIndex, Payload.PlayStateChanged)
|
||||
}
|
||||
|
||||
currentPlaying = audio
|
||||
}
|
||||
|
||||
fun setIsLoadingAudio(isLoading: Boolean, audio: Audio) {
|
||||
if(audio == currentPlaying) {
|
||||
val index = data.indexOf(audio)
|
||||
if(index == -1) return
|
||||
|
||||
data[index].isLoading = isLoading
|
||||
|
||||
notifyItemChanged(index, Payload.IsLoadingChanged)
|
||||
return
|
||||
}
|
||||
val prevIndex = data.indexOf(currentPlaying)
|
||||
|
||||
if(prevIndex != -1) {
|
||||
data[prevIndex].isLoading = false
|
||||
notifyItemChanged(prevIndex, Payload.IsLoadingChanged)
|
||||
}
|
||||
val newIndex = data.indexOf(audio)
|
||||
if(newIndex != -1) {
|
||||
data[newIndex].isLoading = isLoading
|
||||
notifyItemChanged(newIndex, Payload.IsLoadingChanged)
|
||||
}
|
||||
currentPlaying = audio
|
||||
}
|
||||
|
||||
fun updateAudioProgress(total: Int, progress: Int, audio: Audio) {
|
||||
|
||||
}
|
||||
|
||||
inner class AudiosViewHolder(val binding: AudioListItemBinding) : ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AudiosViewHolder {
|
||||
val binding = AudioListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return AudiosViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return data.size
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: AudiosViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
val audio = data[position]
|
||||
if(payloads.isEmpty()) {
|
||||
|
||||
|
||||
holder.binding.audioName.text = audio.name
|
||||
holder.binding.audioAuthor.text = audio.userName
|
||||
holder.binding.thumbnail.load(audio.thumbnail)
|
||||
|
||||
holder.binding.audioItemOptionsButton.setOnClickListener {
|
||||
onOptionsClick(audio, it)
|
||||
}
|
||||
|
||||
holder.binding.playButton.icon = ResourcesCompat.getDrawable(
|
||||
holder.itemView.resources,
|
||||
if(audio.isPlaying) R.drawable.baseline_pause_24 else R.drawable.baseline_play_arrow_24,
|
||||
null
|
||||
)
|
||||
|
||||
holder.binding.loading.visibility = if(audio.isLoading) View.VISIBLE else View.GONE
|
||||
|
||||
holder.binding.playButton.setOnClickListener {
|
||||
onPlayClick(audio)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// only updates play button
|
||||
if(payloads.contains(Payload.PlayStateChanged)) {
|
||||
holder.binding.playButton.icon = ResourcesCompat.getDrawable(
|
||||
holder.itemView.resources,
|
||||
if(audio.isPlaying) R.drawable.baseline_pause_24 else R.drawable.baseline_play_arrow_24,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
if(payloads.contains(Payload.IsLoadingChanged)) {
|
||||
holder.binding.loading.visibility = if(audio.isLoading) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: AudiosViewHolder, position: Int) {}
|
||||
|
||||
fun removeAudio(audio: Audio) {
|
||||
val index = data.indexOf(audio)
|
||||
|
||||
if(index == -1) return
|
||||
// TODO data should be modified from outside
|
||||
data = data.toMutableList().apply {
|
||||
removeAt(index)
|
||||
}
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
package com.isolaatti.audio.audios_list.presentation
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.AudiosRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AudiosViewModel @Inject constructor(private val audiosRepository: AudiosRepository) : ViewModel() {
|
||||
val resource: MutableLiveData<Resource<List<Audio>>> = MutableLiveData()
|
||||
val audioRemoved: MutableLiveData<Audio?> = MutableLiveData()
|
||||
|
||||
fun loadAudios(userId: Int) {
|
||||
viewModelScope.launch {
|
||||
audiosRepository.getAudiosOfUser(userId, null).onEach {
|
||||
resource.postValue(it)
|
||||
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAudio(audio: Audio) {
|
||||
viewModelScope.launch {
|
||||
audiosRepository.deleteAudio(audio.id).onEach {
|
||||
if(it is Resource.Success) {
|
||||
audioRemoved.postValue(audio)
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,173 +0,0 @@
|
||||
package com.isolaatti.audio.audios_list.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.audios_list.presentation.AudiosAdapter
|
||||
import com.isolaatti.audio.audios_list.presentation.AudiosViewModel
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.audio.player.AudioPlayerConnector
|
||||
import com.isolaatti.common.ErrorMessageViewModel
|
||||
import com.isolaatti.databinding.FragmentAudiosBinding
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AudiosFragment : Fragment() {
|
||||
lateinit var viewBinding: FragmentAudiosBinding
|
||||
private val viewModel: AudiosViewModel by viewModels()
|
||||
private val errorViewModel: ErrorMessageViewModel by activityViewModels()
|
||||
private val arguments: AudiosFragmentArgs by navArgs()
|
||||
private lateinit var adapter: AudiosAdapter
|
||||
private var privilegedUserId by Delegates.notNull<Int>()
|
||||
|
||||
private lateinit var audioPlayerConnector: AudioPlayerConnector
|
||||
|
||||
private var loadedFirstTime = false
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
viewBinding = FragmentAudiosBinding.inflate(inflater)
|
||||
|
||||
return viewBinding.root
|
||||
}
|
||||
|
||||
private fun onDeleteAudio(audio: Audio) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.delete_audio_message)
|
||||
.setTitle(R.string.delete_audio_title)
|
||||
.setPositiveButton(R.string.yes_continue) { _, _ ->
|
||||
viewModel.removeAudio(audio)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private val onOptionsClick: ((audio: Audio, button: View) -> Boolean) = { audio, button ->
|
||||
val popup = PopupMenu(requireContext(), button)
|
||||
popup.menuInflater.inflate(R.menu.audio_item_menu, popup.menu)
|
||||
|
||||
if(audio.userId != privilegedUserId) {
|
||||
popup.menu.removeItem(R.id.rename_item)
|
||||
popup.menu.removeItem(R.id.delete_item)
|
||||
popup.menu.removeItem(R.id.set_as_profile_audio)
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener {
|
||||
when(it.itemId) {
|
||||
R.id.delete_item -> {
|
||||
onDeleteAudio(audio)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
popup.show()
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
private val onAudioPlayClick: (audio: Audio) -> Unit = {
|
||||
audioPlayerConnector.playPauseAudio(it)
|
||||
}
|
||||
|
||||
private val audioPlayerConnectorListener: AudioPlayerConnector.Listener = object: AudioPlayerConnector.Listener {
|
||||
override fun onPlaying(isPlaying: Boolean, audio: Playable) {
|
||||
if(audio is Audio)
|
||||
adapter.setIsPlaying(isPlaying, audio)
|
||||
}
|
||||
|
||||
override fun isLoading(isLoading: Boolean, audio: Playable) {
|
||||
if(audio is Audio)
|
||||
adapter.setIsLoadingAudio(isLoading, audio)
|
||||
}
|
||||
|
||||
override fun progressChanged(second: Int, audio: Playable) {
|
||||
|
||||
}
|
||||
|
||||
override fun durationChanged(duration: Int, audio: Playable) {
|
||||
|
||||
}
|
||||
|
||||
override fun onEnded(audio: Playable) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
audioPlayerConnector = AudioPlayerConnector(requireContext())
|
||||
|
||||
audioPlayerConnector.addListener(audioPlayerConnectorListener)
|
||||
|
||||
viewLifecycleOwner.lifecycle.addObserver(audioPlayerConnector)
|
||||
|
||||
adapter = AudiosAdapter(onPlayClick = onAudioPlayClick, onOptionsClick = onOptionsClick)
|
||||
|
||||
viewBinding.recyclerAudios.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
|
||||
viewBinding.recyclerAudios.adapter = adapter
|
||||
|
||||
|
||||
setupObservers()
|
||||
if(arguments.source == SOURCE_PROFILE) {
|
||||
privilegedUserId = arguments.sourceId.toInt()
|
||||
viewModel.loadAudios(privilegedUserId)
|
||||
}
|
||||
|
||||
viewBinding.topAppBar.setNavigationOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.resource.observe(viewLifecycleOwner) { resource ->
|
||||
when(resource) {
|
||||
is Resource.Error -> {
|
||||
errorViewModel.error.postValue(resource.errorType)
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
if(!loadedFirstTime) {
|
||||
viewBinding.progressBarLoading.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
is Resource.Success -> {
|
||||
viewBinding.progressBarLoading.visibility = View.GONE
|
||||
loadedFirstTime = true
|
||||
adapter.setData(resource.data!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.audioRemoved.observe(viewLifecycleOwner) {
|
||||
if(it != null){
|
||||
adapter.removeAudio(it)
|
||||
viewModel.audioRemoved.value = null
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SOURCE_PROFILE = "source_profile"
|
||||
const val SOURCE_SQUAD = "source_squads"
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,6 @@ import retrofit2.http.Query
|
||||
interface AudiosApi {
|
||||
companion object {
|
||||
const val AudioParam= "audioFile"
|
||||
const val NameParam = "name"
|
||||
const val DurationParam = "duration"
|
||||
}
|
||||
@GET("/api/Audios/OfUser/{userId}")
|
||||
@ -21,7 +20,7 @@ interface AudiosApi {
|
||||
|
||||
@POST("/api/Audios/Create")
|
||||
@Multipart
|
||||
fun uploadFile(@Part file: MultipartBody.Part, @Part name: MultipartBody.Part, @Part duration: MultipartBody.Part): Call<AudioDto>
|
||||
fun uploadFile(@Part file: MultipartBody.Part, @Part duration: MultipartBody.Part): Call<AudioDto>
|
||||
|
||||
@POST("/api/Audios/{audioId}/Delete")
|
||||
fun deleteAudio(@Path("audioId") audioId: String): Call<ResultDto<String>>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package com.isolaatti.audio.common.data
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import com.isolaatti.MyApplication
|
||||
import androidx.core.net.toFile
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.AudiosRepository
|
||||
import com.isolaatti.audio.drafts.data.AudiosDraftsDao
|
||||
import com.isolaatti.settings.domain.UserIdSetting
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -13,12 +13,10 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
import retrofit2.awaitResponse
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class AudiosRepositoryImpl @Inject constructor(
|
||||
private val audiosApi: AudiosApi,
|
||||
private val audiosDraftsDao: AudiosDraftsDao,
|
||||
private val userIdSetting: UserIdSetting
|
||||
) : AudiosRepository {
|
||||
companion object {
|
||||
@ -57,18 +55,12 @@ class AudiosRepositoryImpl @Inject constructor(
|
||||
emit(_getAudiosOfUser(userId, lastId))
|
||||
}
|
||||
|
||||
override fun uploadAudio(draftId: Long): Flow<Resource<Audio>> = flow {
|
||||
val audioDraftEntity = audiosDraftsDao.getAudioDraftById(draftId)
|
||||
if(audioDraftEntity == null) {
|
||||
emit(Resource.Error(Resource.Error.ErrorType.NotFoundError, "draft not found"))
|
||||
return@flow
|
||||
}
|
||||
override fun uploadAudio(uri: Uri, postId: Long): Flow<Resource<Audio>> = flow {
|
||||
|
||||
val file = File(MyApplication.myApp.filesDir, audioDraftEntity.audioLocalPath)
|
||||
|
||||
val file = uri.toFile()
|
||||
|
||||
if(!file.exists()) {
|
||||
// remove draft, file was removed from file system for some reason
|
||||
audiosDraftsDao.deleteDrafts(arrayOf(audioDraftEntity))
|
||||
emit(Resource.Error(Resource.Error.ErrorType.OtherError, "File not found"))
|
||||
return@flow
|
||||
}
|
||||
@ -77,10 +69,9 @@ class AudiosRepositoryImpl @Inject constructor(
|
||||
val response = audiosApi.uploadFile(
|
||||
MultipartBody.Part.createFormData(
|
||||
AudiosApi.AudioParam, // api parameter name
|
||||
audioDraftEntity.audioLocalPath.split("/")[1],
|
||||
file.asRequestBody("audio/3gpp".toMediaType()) // actual file to be sent
|
||||
uri.lastPathSegment,
|
||||
file.asRequestBody("audio/aac".toMediaType()) // actual file to be sent
|
||||
),
|
||||
MultipartBody.Part.createFormData(AudiosApi.NameParam, audioDraftEntity.name),
|
||||
MultipartBody.Part.createFormData(AudiosApi.DurationParam, "0")
|
||||
).awaitResponse()
|
||||
|
||||
@ -88,7 +79,6 @@ class AudiosRepositoryImpl @Inject constructor(
|
||||
val audioDto = response.body()
|
||||
if(audioDto != null) {
|
||||
Log.d(LOG_TAG, "emit audio dto")
|
||||
audiosDraftsDao.deleteDrafts(arrayOf(audioDraftEntity))
|
||||
emit(Resource.Success(Audio.fromDto(audioDto)))
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.isolaatti.audio.common.domain
|
||||
|
||||
import android.net.Uri
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@ -7,7 +8,7 @@ interface AudiosRepository {
|
||||
fun getAudiosOfUser(userId: Int, lastId: String?): Flow<Resource<List<Audio>>>
|
||||
fun getMyAudios(lastId: String?): Flow<Resource<List<Audio>>>
|
||||
|
||||
fun uploadAudio(draftId: Long): Flow<Resource<Audio>>
|
||||
fun uploadAudio(uri: Uri, postId: Long): Flow<Resource<Audio>>
|
||||
|
||||
fun deleteAudio(audioId: String): Flow<Resource<Boolean>>
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
package com.isolaatti.audio.common.domain
|
||||
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class UploadAudioUC @Inject constructor(private val audiosRepository: AudiosRepository) {
|
||||
operator fun invoke(draftId: Long): Flow<Resource<Audio>> = audiosRepository.uploadAudio(draftId)
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
package com.isolaatti.audio.drafts.data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "audio_drafts")
|
||||
data class AudioDraftEntity(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long,
|
||||
val name: String,
|
||||
val audioLocalPath: String,
|
||||
val sizeInBytes: Long
|
||||
)
|
||||
@ -1,53 +0,0 @@
|
||||
package com.isolaatti.audio.drafts.data
|
||||
|
||||
import android.util.Log
|
||||
import com.isolaatti.MyApplication
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import java.io.File
|
||||
|
||||
class AudioDraftsRepositoryImpl(private val audiosDraftsDao: AudiosDraftsDao) : AudioDraftsRepository {
|
||||
override fun saveAudioDraft(name: String, relativePath: String, size: Long): Flow<AudioDraft> = flow {
|
||||
val entity = AudioDraftEntity(0, name, relativePath, size)
|
||||
val insertedEntityId = audiosDraftsDao.insertAudioDraft(entity)
|
||||
|
||||
emit(AudioDraft.fromEntity(entity.copy(id = insertedEntityId)))
|
||||
}
|
||||
|
||||
override fun getAudioDrafts(): Flow<List<AudioDraft>> = flow {
|
||||
emit(audiosDraftsDao.getDrafts().map { AudioDraft.fromEntity(it) })
|
||||
}
|
||||
|
||||
override fun deleteDrafts(draftIds: LongArray): Flow<Boolean> = flow {
|
||||
val drafts = audiosDraftsDao.getAudioDraftsByIds(*draftIds)
|
||||
audiosDraftsDao.deleteDrafts(drafts)
|
||||
|
||||
try {
|
||||
for(draft in drafts) {
|
||||
File(MyApplication.myApp.applicationContext.filesDir, draft.audioLocalPath).delete()
|
||||
}
|
||||
} catch(securityException: SecurityException) {
|
||||
Log.e("AudioDraftsRepositoryImpl", "Could not delete file\n${securityException.message}")
|
||||
}
|
||||
emit(true)
|
||||
}
|
||||
|
||||
override fun renameDraft(draftId: Long, name: String): Flow<Boolean> = flow {
|
||||
val rowsAffected = audiosDraftsDao.renameDraft(draftId, name)
|
||||
|
||||
emit(rowsAffected > 0)
|
||||
}
|
||||
|
||||
override fun getAudioDraftById(draftId: Long): Flow<Resource<AudioDraft>> = flow {
|
||||
val audioDraft = audiosDraftsDao.getAudioDraftById(draftId)
|
||||
|
||||
if(audioDraft == null) {
|
||||
emit(Resource.Error(Resource.Error.ErrorType.NotFoundError, "Audio draft not found"))
|
||||
} else {
|
||||
emit(Resource.Success(AudioDraft.fromEntity(audioDraft)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
package com.isolaatti.audio.drafts.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
|
||||
@Dao
|
||||
interface AudiosDraftsDao {
|
||||
@Insert
|
||||
fun insertAudioDraft(audioDraftEntity: AudioDraftEntity): Long
|
||||
|
||||
@Query("SELECT * FROM audio_drafts WHERE id = :draftId")
|
||||
fun getAudioDraftById(draftId: Long): AudioDraftEntity?
|
||||
|
||||
@Query("SELECT * FROM audio_drafts WHERE id in (:draftId)")
|
||||
fun getAudioDraftsByIds(vararg draftId: Long): Array<AudioDraftEntity>
|
||||
|
||||
@Query("SELECT * FROM audio_drafts ORDER BY id DESC")
|
||||
fun getDrafts(): List<AudioDraftEntity>
|
||||
|
||||
@Delete
|
||||
fun deleteDrafts(draft: Array<AudioDraftEntity>)
|
||||
|
||||
@Query("UPDATE audio_drafts SET name = :name WHERE id = :id")
|
||||
fun renameDraft(id: Long, name: String): Int
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
package com.isolaatti.audio.drafts.domain
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.isolaatti.MyApplication
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.audio.drafts.data.AudioDraftEntity
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
|
||||
data class AudioDraft(val id: Long, val name: String, val localStorageRelativePath: String, val size: Long) : Playable(), Serializable {
|
||||
override val thumbnail: String?
|
||||
get() = null
|
||||
|
||||
override val uri: Uri
|
||||
get() {
|
||||
val appFiles = MyApplication.myApp.applicationContext.filesDir
|
||||
return File(appFiles, localStorageRelativePath).toUri()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromEntity(audioDraftEntity: AudioDraftEntity): AudioDraft {
|
||||
return AudioDraft(audioDraftEntity.id, audioDraftEntity.name, audioDraftEntity.audioLocalPath, audioDraftEntity.sizeInBytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
package com.isolaatti.audio.drafts.domain.repository
|
||||
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.utils.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AudioDraftsRepository {
|
||||
fun saveAudioDraft(name: String, relativePath: String, size: Long): Flow<AudioDraft>
|
||||
|
||||
fun getAudioDrafts(): Flow<List<AudioDraft>>
|
||||
|
||||
fun deleteDrafts(draftIds: LongArray): Flow<Boolean>
|
||||
|
||||
fun renameDraft(draftId: Long, name: String): Flow<Boolean>
|
||||
|
||||
fun getAudioDraftById(draftId: Long): Flow<Resource<AudioDraft>>
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
package com.isolaatti.audio.drafts.domain.use_case
|
||||
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class SaveAudioDraft @Inject constructor(private val audioDraftsRepository: AudioDraftsRepository) {
|
||||
operator fun invoke(name: String, relativePath: String, size: Long): Flow<AudioDraft> {
|
||||
return audioDraftsRepository.saveAudioDraft(name, relativePath, size)
|
||||
}
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
package com.isolaatti.audio.drafts.presentation
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.databinding.AudioListItemBinding
|
||||
|
||||
class AudioDraftsAdapter(
|
||||
private val onOptionsClicked: (item: AudioDraft, view: View) -> Unit = { _,_ -> },
|
||||
private val onPlayClicked: (item: AudioDraft , view: View) -> Unit = { _,_ -> },
|
||||
private val onItemClicked: (item: AudioDraft , view: View) -> Unit = { _,_ -> }
|
||||
) : ListAdapter<AudioDraft, AudioDraftsAdapter.AudioDraftViewHolder>(diffCallback) {
|
||||
inner class AudioDraftViewHolder(val audioListItemBinding: AudioListItemBinding) : ViewHolder(audioListItemBinding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AudioDraftViewHolder {
|
||||
return AudioDraftViewHolder(
|
||||
AudioListItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: AudioDraftViewHolder, position: Int) {
|
||||
val item = getItem(position)
|
||||
holder.audioListItemBinding.apply {
|
||||
audioName.text = item.name
|
||||
audioItemOptionsButton.setOnClickListener { onOptionsClicked(item, it) }
|
||||
playButton.icon = ResourcesCompat.getDrawable(
|
||||
holder.itemView.resources,
|
||||
if(item.isPlaying) R.drawable.baseline_pause_24 else R.drawable.baseline_play_arrow_24,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val diffCallback = object: DiffUtil.ItemCallback<AudioDraft>() {
|
||||
override fun areItemsTheSame(oldItem: AudioDraft, newItem: AudioDraft): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: AudioDraft, newItem: AudioDraft): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
package com.isolaatti.audio.drafts.presentation
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class AudioDraftsViewModel : ViewModel() {
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
package com.isolaatti.audio.drafts.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.audio.drafts.presentation.AudioDraftsAdapter
|
||||
import com.isolaatti.audio.drafts.presentation.AudioDraftsViewModel
|
||||
import com.isolaatti.databinding.FragmentAudioDraftsBinding
|
||||
|
||||
class AudioDraftsFragment : Fragment() {
|
||||
private lateinit var binding: FragmentAudioDraftsBinding
|
||||
private val viewModel: AudioDraftsViewModel by viewModels()
|
||||
|
||||
private var adapter: AudioDraftsAdapter? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentAudioDraftsBinding.inflate(inflater)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
adapter = AudioDraftsAdapter(
|
||||
onPlayClicked = { item: AudioDraft, view: View ->
|
||||
|
||||
|
||||
},
|
||||
onOptionsClicked = { item, button ->
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
binding.recycler.adapter = adapter
|
||||
binding.recycler.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,206 +0,0 @@
|
||||
package com.isolaatti.audio.player
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.datasource.DefaultDataSource
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class AudioPlayerConnector(
|
||||
private val context: Context
|
||||
): LifecycleEventObserver {
|
||||
companion object {
|
||||
const val TAG = "AudioPlayerConnector"
|
||||
}
|
||||
private var player: Player? = null
|
||||
private var audio: Playable? = null
|
||||
private var mediaItem: MediaItem? = null
|
||||
private var ended = false
|
||||
|
||||
private val listeners: MutableList<Listener> = mutableListOf()
|
||||
|
||||
private val scheduleExecutorService = Executors.newScheduledThreadPool(1)
|
||||
private var timerFuture: ScheduledFuture<*>? = null
|
||||
|
||||
private val timerRunnable = Runnable {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
audio?.let {
|
||||
val progress = (player?.currentPosition)
|
||||
if(progress != null) {
|
||||
listeners.forEach { listener -> listener.progressChanged((progress / 1000).toInt(), it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun startTimer() {
|
||||
Log.d(TAG, "startTimer()")
|
||||
if(timerFuture == null) {
|
||||
timerFuture = scheduleExecutorService.scheduleAtFixedRate(timerRunnable, 0, 100, TimeUnit.MILLISECONDS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopTimer() {
|
||||
timerFuture?.cancel(false)
|
||||
timerFuture = null
|
||||
}
|
||||
|
||||
private val playerListener: Player.Listener = object: Player.Listener {
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
Log.e(TAG, error.message.toString())
|
||||
}
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if(audio != null) {
|
||||
listeners.forEach { listener -> listener.onPlaying(isPlaying, audio!!)}
|
||||
}
|
||||
if(isPlaying) {
|
||||
ended = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsLoadingChanged(isLoading: Boolean) {
|
||||
audio?.let {
|
||||
listeners.forEach { listener -> listener.isLoading(isLoading, it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
when(playbackState) {
|
||||
Player.STATE_ENDED -> {
|
||||
Log.d(TAG, "STATE_ENDED")
|
||||
audio?.let {
|
||||
listeners.forEach { listener ->
|
||||
listener.onPlaying(false, it)
|
||||
listener.onEnded(it)
|
||||
}
|
||||
}
|
||||
stopTimer()
|
||||
ended = true
|
||||
}
|
||||
Player.STATE_BUFFERING -> {
|
||||
player?.totalBufferedDuration?.let {
|
||||
val seconds = (it / 1000).toInt()
|
||||
Log.d(TAG, "Duration $it")
|
||||
audio?.let {
|
||||
listeners.forEach { listener -> listener.durationChanged(seconds, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Player.STATE_IDLE -> {}
|
||||
Player.STATE_READY -> {
|
||||
Log.d(TAG, "STATE_READY")
|
||||
player?.totalBufferedDuration?.let {
|
||||
val seconds = (it / 1000).toInt()
|
||||
Log.d(TAG, "Duration $it")
|
||||
audio?.let {
|
||||
listeners.forEach { listener -> listener.durationChanged(seconds, it) }
|
||||
}
|
||||
}
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializePlayer() {
|
||||
player = ExoPlayer.Builder(context).build()
|
||||
player?.addListener(playerListener)
|
||||
player?.prepare()
|
||||
}
|
||||
|
||||
fun addListener(listener: Listener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
private fun releasePlayer() {
|
||||
player?.run {
|
||||
release()
|
||||
}
|
||||
player = null
|
||||
stopTimer()
|
||||
}
|
||||
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
Log.d(TAG, event.toString())
|
||||
when(event) {
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
if(player == null) {
|
||||
initializePlayer()
|
||||
}
|
||||
}
|
||||
Lifecycle.Event.ON_DESTROY -> {
|
||||
releasePlayer()
|
||||
listeners.clear()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
fun playPauseAudio(audio: Playable) {
|
||||
|
||||
// intention is to pause current audio
|
||||
if(audio == this.audio && player?.isPlaying == true) {
|
||||
player?.pause()
|
||||
return
|
||||
} else if(audio == this.audio) {
|
||||
if(ended) {
|
||||
player?.seekTo(0)
|
||||
ended = false
|
||||
return
|
||||
}
|
||||
player?.play()
|
||||
return
|
||||
}
|
||||
this.audio = audio
|
||||
mediaItem = MediaItem.fromUri(audio.uri)
|
||||
|
||||
player?.setMediaItem(mediaItem!!)
|
||||
player?.playWhenReady = true
|
||||
}
|
||||
|
||||
fun stopPlayback() {
|
||||
ended = true
|
||||
player?.pause()
|
||||
stopTimer()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onPlaying(isPlaying: Boolean, audio: Playable)
|
||||
fun isLoading(isLoading: Boolean, audio: Playable)
|
||||
fun progressChanged(second: Int, audio: Playable)
|
||||
fun durationChanged(duration: Int, audio: Playable)
|
||||
fun onEnded(audio: Playable)
|
||||
}
|
||||
|
||||
open class DefaultListener() : Listener {
|
||||
override fun onPlaying(isPlaying: Boolean, audio: Playable) {}
|
||||
override fun isLoading(isLoading: Boolean, audio: Playable) {}
|
||||
override fun progressChanged(second: Int, audio: Playable) {}
|
||||
override fun durationChanged(duration: Int, audio: Playable) {}
|
||||
override fun onEnded(audio: Playable) {}
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
package com.isolaatti.audio.player
|
||||
|
||||
import androidx.media3.common.Player
|
||||
|
||||
abstract class PlayerFactory {
|
||||
abstract fun MakePlayer(): Player
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
package com.isolaatti.audio.recorder.presentation
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.audio.drafts.domain.use_case.SaveAudioDraft
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AudioRecorderViewModel @Inject constructor(
|
||||
private val saveAudioDraftUC: SaveAudioDraft
|
||||
) : ViewModel() {
|
||||
var name: String = ""
|
||||
set(value) {
|
||||
field = value
|
||||
validate()
|
||||
}
|
||||
var relativePath = ""
|
||||
var size: Long = 0
|
||||
private var audioRecorded = false
|
||||
|
||||
val audioDraft: MutableLiveData<AudioDraft> = MutableLiveData()
|
||||
|
||||
val canSave: MutableLiveData<Boolean> = MutableLiveData()
|
||||
|
||||
private fun validate() {
|
||||
canSave.value = audioRecorded && name.isNotBlank()
|
||||
}
|
||||
|
||||
fun onAudioRecorded() {
|
||||
audioRecorded = true
|
||||
validate()
|
||||
}
|
||||
|
||||
fun onClearAudio() {
|
||||
audioRecorded = false
|
||||
validate()
|
||||
}
|
||||
fun saveAudioDraft() {
|
||||
viewModelScope.launch {
|
||||
saveAudioDraftUC(name, relativePath, size).onEach {
|
||||
audioDraft.postValue(it)
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
package com.isolaatti.audio.recorder.presentation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.isolaatti.audio.drafts.ui.AudioDraftsFragment
|
||||
import com.isolaatti.audio.recorder.ui.AudioRecorderHelperNotesFragment
|
||||
|
||||
class RecorderPagerAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
|
||||
override fun getItemCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> AudioRecorderHelperNotesFragment()
|
||||
1 -> AudioDraftsFragment()
|
||||
else -> Fragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,337 +0,0 @@
|
||||
package com.isolaatti.audio.recorder.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Resources.Theme
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaRecorder
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.PermissionChecker
|
||||
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.get
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.dialog.MaterialDialogs
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.audio.player.AudioPlayerConnector
|
||||
import com.isolaatti.audio.recorder.presentation.AudioRecorderViewModel
|
||||
import com.isolaatti.audio.recorder.presentation.RecorderPagerAdapter
|
||||
import com.isolaatti.databinding.ActivityAudioRecorderBinding
|
||||
import com.isolaatti.utils.Resource
|
||||
import com.isolaatti.utils.clockFormat
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.UUID
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AudioRecorderActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
const val LOG_TAG = "AudioRecorderActivity"
|
||||
|
||||
const val IN_EXTRA_DRAFT_ID = "in_draft_id"
|
||||
const val OUT_EXTRA_DRAFT = "out_draft"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityAudioRecorderBinding
|
||||
|
||||
private var audioRecorder: MediaRecorder? = null
|
||||
private var recordingPaused = false
|
||||
private var audioPlayerConnector: AudioPlayerConnector? = null
|
||||
|
||||
private val viewModel: AudioRecorderViewModel by viewModels()
|
||||
private lateinit var outputFile: String
|
||||
|
||||
private val requestAudioPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
|
||||
if(it) {
|
||||
startRecording()
|
||||
} else {
|
||||
showPermissionRationale()
|
||||
}
|
||||
}
|
||||
|
||||
private val audioPlayerListener = object: AudioPlayerConnector.DefaultListener() {
|
||||
override fun onPlaying(isPlaying: Boolean, audio: Playable) {
|
||||
if(isPlaying) {
|
||||
binding.seekbar.isEnabled = true
|
||||
binding.playPauseButton.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_pause_24, theme)
|
||||
} else {
|
||||
binding.playPauseButton.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_play_arrow_24, theme)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onEnded(audio: Playable) {
|
||||
binding.seekbar.isEnabled = false
|
||||
binding.seekbar.progress = 0
|
||||
}
|
||||
|
||||
override fun durationChanged(duration: Int, audio: Playable) {
|
||||
super.durationChanged(duration, audio)
|
||||
binding.seekbar.max = duration
|
||||
totalTime = duration
|
||||
}
|
||||
|
||||
override fun progressChanged(second: Int, audio: Playable) {
|
||||
binding.seekbar.progress = second
|
||||
setDisplayTime(second, true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityAudioRecorderBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
// Path where results are stored
|
||||
File("${filesDir.absolutePath}/audios/").let {
|
||||
if(!it.isDirectory) {
|
||||
it.mkdir()
|
||||
}
|
||||
}
|
||||
|
||||
binding.seekbar.isEnabled = false
|
||||
outputFile = "${cacheDir.absolutePath}/audio_recorder.3gp"
|
||||
|
||||
setupListeners()
|
||||
setupObservers()
|
||||
|
||||
audioPlayerConnector = AudioPlayerConnector(this)
|
||||
audioPlayerConnector?.addListener(audioPlayerListener)
|
||||
|
||||
lifecycle.addObserver(audioPlayerConnector!!)
|
||||
|
||||
}
|
||||
|
||||
private fun checkRecordAudioPermission(): Boolean {
|
||||
return when {
|
||||
PermissionChecker.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PERMISSION_GRANTED -> true
|
||||
ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO) -> {
|
||||
showPermissionRationale()
|
||||
false
|
||||
}
|
||||
|
||||
else -> {
|
||||
requestAudioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPermissionRationale() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle("Record audio permission")
|
||||
.setMessage("We need permission to access your microphone so that you can record your audio. Go to settings.")
|
||||
.setPositiveButton("Go to settings"){_, _ ->
|
||||
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
|
||||
intent.data = Uri.fromParts("package", packageName, null)
|
||||
startActivity(intent)
|
||||
}
|
||||
.setNegativeButton("No, thanks", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
private fun setupListeners() {
|
||||
binding.recordButton.setOnClickListener {
|
||||
if(checkRecordAudioPermission()) {
|
||||
Log.d(LOG_TAG, "Starts recording")
|
||||
startRecording()
|
||||
} else {
|
||||
Log.d(LOG_TAG, "Failed to start recording: mic permission not granted")
|
||||
}
|
||||
}
|
||||
binding.stopRecording.setOnClickListener {
|
||||
stopRecording()
|
||||
}
|
||||
|
||||
binding.cancelButton.setOnClickListener {
|
||||
discardRecording()
|
||||
}
|
||||
|
||||
binding.pauseRecording.setOnClickListener {
|
||||
pauseRecording()
|
||||
}
|
||||
|
||||
binding.playPauseButton.setOnClickListener {
|
||||
playPauseRecording(object: Playable() {
|
||||
override val uri: Uri
|
||||
get() = outputFile.toUri()
|
||||
override val thumbnail: String?
|
||||
get() = null
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
binding.inputDraftName.editText?.doOnTextChanged { text, _, _, _ ->
|
||||
viewModel.name = text.toString()
|
||||
}
|
||||
|
||||
binding.toolbar.setOnMenuItemClickListener {
|
||||
when(it.itemId) {
|
||||
R.id.save_draft -> {
|
||||
|
||||
viewModel.relativePath = "/audios/${UUID.randomUUID()}.3gp"
|
||||
File(outputFile).run {
|
||||
viewModel.size = length()
|
||||
copyTo(File(filesDir.absolutePath, viewModel.relativePath), overwrite = true)
|
||||
}
|
||||
|
||||
viewModel.saveAudioDraft()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.canSave.observe(this) {
|
||||
binding.toolbar.menu.findItem(R.id.save_draft).isEnabled = it
|
||||
}
|
||||
|
||||
// audio draft is saved!
|
||||
viewModel.audioDraft.observe(this) {
|
||||
val result = Intent().apply {
|
||||
putExtra(OUT_EXTRA_DRAFT, it)
|
||||
}
|
||||
setResult(RESULT_OK, result)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
// region timer
|
||||
private var timer: Job? = null
|
||||
private var timerValue = 0
|
||||
private var totalTime = 0
|
||||
|
||||
private fun setDisplayTime(seconds: Int, showTotalTime: Boolean) {
|
||||
binding.time.text = buildString {
|
||||
append(seconds.clockFormat())
|
||||
|
||||
if(showTotalTime) {
|
||||
append("/")
|
||||
append(totalTime.clockFormat())
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun startTimerRecorder() {
|
||||
|
||||
timer = CoroutineScope(Dispatchers.Main).launch {
|
||||
setDisplayTime(timerValue, false)
|
||||
delay(1000)
|
||||
timerValue++
|
||||
startTimerRecorder()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun stopTimer() {
|
||||
timer?.cancel()
|
||||
}
|
||||
|
||||
// end region
|
||||
|
||||
// region record functions
|
||||
private fun startRecording() {
|
||||
viewModel.onClearAudio()
|
||||
recordingPaused = false
|
||||
audioRecorder = MediaRecorder().apply {
|
||||
setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||
setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP)
|
||||
setOutputFile(outputFile)
|
||||
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
|
||||
|
||||
try {
|
||||
prepare()
|
||||
start()
|
||||
timerValue = 0
|
||||
startTimerRecorder()
|
||||
binding.viewAnimator.displayedChild = 1
|
||||
|
||||
} catch(e: IOException) {
|
||||
Log.e(LOG_TAG, "prepare() failed\n${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopRecording() {
|
||||
audioRecorder?.apply {
|
||||
stop()
|
||||
release()
|
||||
}
|
||||
stopTimer()
|
||||
audioRecorder = null
|
||||
|
||||
// shows third state: audio recorded
|
||||
binding.viewAnimator.displayedChild = 2
|
||||
recordingPaused = false
|
||||
binding.pauseRecording.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_pause_24, theme)
|
||||
binding.pauseRecording.setIconTintResource(com.google.android.material.R.color.m3_icon_button_icon_color_selector)
|
||||
viewModel.onAudioRecorded()
|
||||
}
|
||||
|
||||
private fun pauseRecording() {
|
||||
if(recordingPaused) {
|
||||
binding.pauseRecording.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_pause_24, theme)
|
||||
binding.pauseRecording.setIconTintResource(com.google.android.material.R.color.m3_icon_button_icon_color_selector)
|
||||
audioRecorder?.resume()
|
||||
startTimerRecorder()
|
||||
} else {
|
||||
binding.pauseRecording.icon = ResourcesCompat.getDrawable(resources, R.drawable.baseline_circle_24, theme)
|
||||
binding.pauseRecording.setIconTintResource(R.color.danger)
|
||||
audioRecorder?.pause()
|
||||
stopTimer()
|
||||
}
|
||||
recordingPaused = !recordingPaused
|
||||
}
|
||||
|
||||
private fun discardRecording(){
|
||||
File(outputFile).apply {
|
||||
try {
|
||||
delete()
|
||||
} catch(e: SecurityException) {
|
||||
Log.e(LOG_TAG, "Could not delete file\n${e.message}")
|
||||
}
|
||||
}
|
||||
binding.viewAnimator.displayedChild = 0
|
||||
viewModel.onClearAudio()
|
||||
totalTime = 0
|
||||
timerValue = 0
|
||||
binding.time.text = ""
|
||||
audioPlayerConnector?.stopPlayback()
|
||||
}
|
||||
|
||||
// end region
|
||||
|
||||
private fun playPauseRecording(audioDraft: Playable) {
|
||||
audioPlayerConnector?.playPauseAudio(audioDraft)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
package com.isolaatti.audio.recorder.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
|
||||
class AudioRecorderContract : ActivityResultContract<Long?, AudioDraft?>() {
|
||||
|
||||
override fun createIntent(context: Context, input: Long?): Intent {
|
||||
val intent = Intent(context, AudioRecorderActivity::class.java).apply {
|
||||
if(input != null) {
|
||||
putExtra(AudioRecorderActivity.IN_EXTRA_DRAFT_ID, input)
|
||||
}
|
||||
}
|
||||
|
||||
return intent
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): AudioDraft? {
|
||||
return when(resultCode){
|
||||
Activity.RESULT_OK -> {
|
||||
intent?.getSerializableExtra(AudioRecorderActivity.OUT_EXTRA_DRAFT) as? AudioDraft
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
package com.isolaatti.audio.recorder.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
class AudioRecorderHelperNotesFragment : Fragment() {
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return LinearLayout(requireContext()).apply {
|
||||
addView(TextView(requireContext()).apply {
|
||||
text = "Allow the user to place some info here to be " +
|
||||
"reading while recording. If this screen is launched from comments, show here the post"
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,6 @@ package com.isolaatti.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.isolaatti.audio.drafts.data.AudioDraftEntity
|
||||
import com.isolaatti.audio.drafts.data.AudiosDraftsDao
|
||||
import com.isolaatti.auth.data.local.UserInfoDao
|
||||
import com.isolaatti.auth.data.local.UserInfoEntity
|
||||
import com.isolaatti.images.common.data.dao.ImagesDraftsDao
|
||||
@ -13,11 +11,10 @@ import com.isolaatti.search.data.SearchHistoryEntity
|
||||
import com.isolaatti.settings.data.KeyValueDao
|
||||
import com.isolaatti.settings.data.KeyValueEntity
|
||||
|
||||
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class, AudioDraftEntity::class, SearchHistoryEntity::class, ImageDraftEntity::class], version = 8)
|
||||
@Database(entities = [KeyValueEntity::class, UserInfoEntity::class, SearchHistoryEntity::class, ImageDraftEntity::class], version = 9)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun keyValueDao(): KeyValueDao
|
||||
abstract fun userInfoDao(): UserInfoDao
|
||||
abstract fun audioDrafts(): AudiosDraftsDao
|
||||
abstract fun searchHistoryDao(): SearchDao
|
||||
abstract fun imagesDraftsDao(): ImagesDraftsDao
|
||||
}
|
||||
@ -91,18 +91,24 @@ fun ImagesRow(
|
||||
|
||||
Card(modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.size(120.dp)) {
|
||||
.size(120.dp),
|
||||
onClick = {
|
||||
onClick(index)
|
||||
}) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
|
||||
) {
|
||||
FilledTonalIconButton(
|
||||
onClick = { onDeleteClick(index) },
|
||||
modifier = Modifier.zIndex(2f)
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Close, contentDescription = null)
|
||||
if(deletable) {
|
||||
FilledTonalIconButton(
|
||||
onClick = { onDeleteClick(index) },
|
||||
modifier = Modifier.zIndex(2f)
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Close, contentDescription = null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
AsyncImage(
|
||||
model = images[index],
|
||||
contentDescription = null,
|
||||
@ -118,5 +124,5 @@ fun ImagesRow(
|
||||
@Preview(device = Devices.PIXEL_5)
|
||||
@Composable
|
||||
fun ImagesRowPreview() {
|
||||
ImagesRow(images = emptyList(), deletable = false, addable = true, onClick = {})
|
||||
ImagesRow(images = emptyList(), deletable = true, addable = true, onClick = {})
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
package com.isolaatti.images.image_chooser.presentation
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.auth.domain.AuthRepository
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.repository.ImagesRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ImageChooserViewModel @Inject constructor(private val authRepository: AuthRepository) : ViewModel() {
|
||||
|
||||
val userId: MutableLiveData<Int> = MutableLiveData()
|
||||
var selectedImage: Image? = null
|
||||
|
||||
val choose: MutableLiveData<Boolean> = MutableLiveData()
|
||||
|
||||
fun getUserId() {
|
||||
viewModelScope.launch {
|
||||
authRepository.getUserId().onEach {
|
||||
userId.postValue(it)
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
package com.isolaatti.images.image_chooser.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.viewModels
|
||||
import com.isolaatti.common.IsolaattiBaseActivity
|
||||
import com.isolaatti.databinding.ActivityImageChooserBinding
|
||||
import com.isolaatti.images.image_chooser.presentation.ImageChooserViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ImageChooserActivity : IsolaattiBaseActivity() {
|
||||
private lateinit var binding: ActivityImageChooserBinding
|
||||
private val viewModel: ImageChooserViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityImageChooserBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
setupObservers()
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.choose.observe(this) {
|
||||
if(it == true && viewModel.selectedImage != null) {
|
||||
viewModel.choose.value = false
|
||||
val resultIntent = Intent().apply {
|
||||
putExtra(OUTPUT_EXTRA_IMAGE, viewModel.selectedImage)
|
||||
}
|
||||
|
||||
setResult(RESULT_OK, resultIntent)
|
||||
finish()
|
||||
return@observe
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val INPUT_EXTRA = "requester"
|
||||
const val OUTPUT_EXTRA_IMAGE = "output_image"
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
package com.isolaatti.images.image_chooser.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.core.content.IntentCompat
|
||||
import com.isolaatti.images.common.data.entity.ImageDraftEntity
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
|
||||
class ImageChooserContract : ActivityResultContract<ImageChooserContract.Requester, ImageDraftEntity?>() {
|
||||
|
||||
enum class Requester {
|
||||
UserPost, SquadPost
|
||||
}
|
||||
override fun createIntent(context: Context, input: Requester): Intent {
|
||||
return Intent(context, ImageChooserActivity::class.java).apply {
|
||||
putExtra(ImageChooserActivity.INPUT_EXTRA, input)
|
||||
}
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): ImageDraftEntity? {
|
||||
if(resultCode != Activity.RESULT_OK) { return null }
|
||||
|
||||
if(intent == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return IntentCompat.getSerializableExtra(intent, ImageChooserActivity.OUTPUT_EXTRA_IMAGE, ImageDraftEntity::class.java)
|
||||
}
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
package com.isolaatti.images.image_chooser.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.isolaatti.MyApplication
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.databinding.FragmentImageChooserMainBinding
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.image_chooser.presentation.ImageChooserViewModel
|
||||
import com.isolaatti.images.image_list.presentation.ImageListViewModel
|
||||
import com.isolaatti.images.image_list.presentation.ImagesAdapter
|
||||
import com.isolaatti.images.image_maker.ui.ImageMakerContract
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ImageChooserMainFragment : Fragment() {
|
||||
private lateinit var binding: FragmentImageChooserMainBinding
|
||||
private val viewModel: ImageChooserViewModel by activityViewModels()
|
||||
private val imagesListViewModel: ImageListViewModel by viewModels()
|
||||
private lateinit var adapter: ImagesAdapter
|
||||
private var cameraPhotoUri: Uri? = null
|
||||
|
||||
private val imageOnClick: (images: List<Image>, position: Int) -> Unit = { images, position ->
|
||||
viewModel.selectedImage = images[position]
|
||||
findNavController().navigate(ImageChooserMainFragmentDirections.actionImageChooserMainFragmentToImageChooserPreview())
|
||||
}
|
||||
|
||||
private fun makePhotoUri(): Uri {
|
||||
val cacheFile = File(requireContext().filesDir, "temp_picture_${Calendar.getInstance().timeInMillis}")
|
||||
return FileProvider.getUriForFile(requireContext(), "${MyApplication.myApp.packageName}.provider", cacheFile)
|
||||
}
|
||||
|
||||
private val imageMakerLauncher = registerForActivityResult(ImageMakerContract()) { image ->
|
||||
image?.also {
|
||||
viewModel.selectedImage = it
|
||||
viewModel.choose.value = true
|
||||
}
|
||||
}
|
||||
|
||||
private val choosePictureLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
|
||||
if(it != null) {
|
||||
imageMakerLauncher.launch(it)
|
||||
}
|
||||
}
|
||||
|
||||
private val takePhotoLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) {
|
||||
if(it && cameraPhotoUri != null) {
|
||||
imageMakerLauncher.launch(cameraPhotoUri)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewModel.getUserId()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentImageChooserMainBinding.inflate(inflater)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
adapter = ImagesAdapter(
|
||||
imageOnClick = imageOnClick,
|
||||
itemWidth = Resources.getSystem().displayMetrics.widthPixels/3
|
||||
)
|
||||
|
||||
binding.recycler.layoutManager =
|
||||
GridLayoutManager(requireContext(), 3, GridLayoutManager.VERTICAL, false)
|
||||
binding.recycler.adapter = adapter
|
||||
|
||||
setupObservers()
|
||||
setupListeners()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
requireActivity().run {
|
||||
setResult(Activity.RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
binding.takePhoto.setOnClickListener {
|
||||
cameraPhotoUri = makePhotoUri()
|
||||
takePhotoLauncher.launch(cameraPhotoUri)
|
||||
}
|
||||
|
||||
binding.uploadPhoto.setOnClickListener {
|
||||
choosePictureLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.userId.observe(viewLifecycleOwner) {
|
||||
imagesListViewModel.userId = it
|
||||
imagesListViewModel.loadNext()
|
||||
Log.d("*****", "Se obtiene userId $it")
|
||||
}
|
||||
|
||||
imagesListViewModel.liveList.observe(viewLifecycleOwner) { imageList ->
|
||||
Log.d("*****", "Se obtiene lista $imageList")
|
||||
adapter.submitList(imageList)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
package com.isolaatti.images.image_chooser.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import coil.load
|
||||
import com.isolaatti.databinding.FragmentImageChooserPreviewBinding
|
||||
import com.isolaatti.images.image_chooser.presentation.ImageChooserViewModel
|
||||
|
||||
class ImageChooserPreview : Fragment() {
|
||||
private lateinit var binding: FragmentImageChooserPreviewBinding
|
||||
private val viewModel: ImageChooserViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentImageChooserPreviewBinding.inflate(inflater)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun showLoading(show: Boolean) {
|
||||
binding.chooseImageButton.visibility = if(show) View.INVISIBLE else View.VISIBLE
|
||||
binding.progressBarLoading.visibility = if(show) View.VISIBLE else View.INVISIBLE
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
binding.image.load(viewModel.selectedImage?.imageUrl)
|
||||
|
||||
binding.chooseImageButton.setOnClickListener {
|
||||
showLoading(true)
|
||||
viewModel.choose.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
package com.isolaatti.images.image_list.presentation
|
||||
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
|
||||
interface ImageAdapterItem {
|
||||
val image: Image?
|
||||
val addImage: Boolean
|
||||
|
||||
companion object {
|
||||
val AddImagePlaceholder: ImageAdapterItem = object: ImageAdapterItem {
|
||||
override val image: Image?
|
||||
get() = null
|
||||
|
||||
override val addImage: Boolean
|
||||
get() = true
|
||||
}
|
||||
|
||||
fun fromImage(image: Image): ImageAdapterItem {
|
||||
return object: ImageAdapterItem {
|
||||
override val addImage: Boolean
|
||||
get() = false
|
||||
|
||||
override val image: Image?
|
||||
get() = image
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,109 +0,0 @@
|
||||
package com.isolaatti.images.image_list.presentation
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.auth.domain.AuthRepository
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.common.domain.repository.ImagesRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
@HiltViewModel
|
||||
class ImageListViewModel @Inject constructor(private val imagesRepository: ImagesRepository, private val authRepository: AuthRepository) : ViewModel() {
|
||||
|
||||
enum class Event {
|
||||
REMOVED_IMAGE, ADDED_IMAGE_BEGINNING
|
||||
}
|
||||
|
||||
val liveList: MutableLiveData<List<Image>> = MutableLiveData(listOf())
|
||||
val error: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
|
||||
val loading: MutableLiveData<Boolean> = MutableLiveData()
|
||||
val deleting: MutableLiveData<Boolean> = MutableLiveData()
|
||||
var noMoreContent = false
|
||||
var lastEvent: Event? = null
|
||||
private var loadedFirstTime = false
|
||||
var userId by Delegates.notNull<Int>()
|
||||
|
||||
val isUserItself: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||
|
||||
private val list: List<Image> get() {
|
||||
return liveList.value ?: listOf()
|
||||
}
|
||||
|
||||
private val lastId: String? get() {
|
||||
return list.lastOrNull()?.id
|
||||
}
|
||||
|
||||
fun addImageAtTheBeginning(image: Image) {
|
||||
liveList.value = listOf(image) + list
|
||||
}
|
||||
|
||||
fun loadNext() {
|
||||
viewModelScope.launch {
|
||||
|
||||
imagesRepository.getImagesOfUser(userId, lastId).onEach { resource ->
|
||||
when(resource) {
|
||||
is Resource.Error -> {
|
||||
error.postValue(resource.errorType)
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
if(!loadedFirstTime) {
|
||||
loading.postValue(true)
|
||||
}
|
||||
}
|
||||
is Resource.Success -> {
|
||||
loading.postValue(false)
|
||||
noMoreContent = resource.data?.isEmpty() == true
|
||||
loadedFirstTime = true
|
||||
if(noMoreContent) {
|
||||
return@onEach
|
||||
}
|
||||
|
||||
liveList.postValue(list + (resource.data ?: listOf()))
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
liveList.value = listOf()
|
||||
loadNext()
|
||||
}
|
||||
|
||||
fun removeImages(images: List<Image>) {
|
||||
viewModelScope.launch {
|
||||
imagesRepository.deleteImages(images).onEach {
|
||||
when(it) {
|
||||
is Resource.Error -> {
|
||||
deleting.postValue(false)
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
deleting.postValue(true)
|
||||
}
|
||||
is Resource.Success -> {
|
||||
deleting.postValue(false)
|
||||
lastEvent = Event.REMOVED_IMAGE
|
||||
liveList.postValue(list.filterNot { image -> images.contains(image) })
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
fun getUserId() {
|
||||
viewModelScope.launch {
|
||||
authRepository.getUserId().onEach {
|
||||
isUserItself.postValue(userId == it)
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
package com.isolaatti.images.image_list.presentation
|
||||
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||
import coil.load
|
||||
import com.isolaatti.common.CoilImageLoader.imageLoader
|
||||
import com.isolaatti.databinding.ImageItemBinding
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
|
||||
class ImagesAdapter(
|
||||
private val imageOnClick: ((images: List<Image>, position: Int) -> Unit),
|
||||
private val itemWidth: Int,
|
||||
private val onImageSelectedCountUpdate: ((count: Int) -> Unit)? = null,
|
||||
private val onDeleteMode: ((enabled: Boolean) -> Unit)? = null,
|
||||
private val onContentRequested: (() -> Unit)? = null) : ListAdapter<Image, ImagesAdapter.ImageViewHolder>(diffCallback){
|
||||
|
||||
companion object {
|
||||
val diffCallback = object: DiffUtil.ItemCallback<Image>() {
|
||||
override fun areItemsTheSame(oldItem: Image, newItem: Image): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Image, newItem: Image): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var deleteMode: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
if(!value) {
|
||||
currentList.forEach { it.delete = false }
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
inner class ImageViewHolder(val imageItemBinding: ImageItemBinding) : RecyclerView.ViewHolder(imageItemBinding.root)
|
||||
|
||||
fun getSelectedImages(): List<Image> {
|
||||
return currentList.filter { it.delete }
|
||||
}
|
||||
|
||||
override fun onCurrentListChanged(
|
||||
previousList: MutableList<Image>,
|
||||
currentList: MutableList<Image>
|
||||
) {
|
||||
super.onCurrentListChanged(previousList, currentList)
|
||||
noMoreContent = (currentList.size - previousList.size) == 0
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = ImageItemBinding.inflate(inflater)
|
||||
|
||||
binding.root.layoutParams = LinearLayout.LayoutParams(itemWidth, itemWidth)
|
||||
return ImageViewHolder(binding)
|
||||
}
|
||||
|
||||
private var requestedNewContent = false
|
||||
private var noMoreContent = false
|
||||
|
||||
/**
|
||||
* Call this method when new content has been added on onLoadMore() callback
|
||||
*/
|
||||
fun newContentRequestFinished() {
|
||||
requestedNewContent = false
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||
val image = getItem(position)
|
||||
|
||||
holder.imageItemBinding.image.load(image.reducedImageUrl, imageLoader)
|
||||
|
||||
if(deleteMode) {
|
||||
holder.imageItemBinding.imageCheckbox.visibility = View.VISIBLE
|
||||
holder.imageItemBinding.imageOverlay.visibility = View.VISIBLE
|
||||
holder.imageItemBinding.root.setOnClickListener {
|
||||
holder.imageItemBinding.imageCheckbox.isChecked = !holder.imageItemBinding.imageCheckbox.isChecked
|
||||
}
|
||||
holder.imageItemBinding.imageCheckbox.setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
image.delete = isChecked
|
||||
|
||||
onImageSelectedCountUpdate?.invoke(currentList.count { it.delete })
|
||||
}
|
||||
holder.imageItemBinding.imageCheckbox.isChecked = image.delete
|
||||
holder.imageItemBinding.root.setOnLongClickListener(null)
|
||||
} else {
|
||||
holder.imageItemBinding.imageCheckbox.visibility = View.GONE
|
||||
holder.imageItemBinding.imageCheckbox.isChecked = false
|
||||
holder.imageItemBinding.imageOverlay.visibility = View.GONE
|
||||
holder.imageItemBinding.root.setOnClickListener {
|
||||
imageOnClick(currentList, position)
|
||||
}
|
||||
holder.imageItemBinding.imageCheckbox.setOnCheckedChangeListener(null)
|
||||
holder.imageItemBinding.root.setOnLongClickListener {
|
||||
image.delete = true
|
||||
onDeleteMode?.invoke(true)
|
||||
onImageSelectedCountUpdate?.invoke(currentList.count { it.delete })
|
||||
true
|
||||
}
|
||||
}
|
||||
val totalItems = currentList.size
|
||||
if(totalItems > 0 && !requestedNewContent && !noMoreContent) {
|
||||
Log.d("ImagesAdapter", "Total items: $totalItems")
|
||||
if(position == totalItems - 1) {
|
||||
requestedNewContent = true
|
||||
onContentRequested?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,258 +0,0 @@
|
||||
package com.isolaatti.images.image_list.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.ActionMode
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.isolaatti.MyApplication
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.common.ErrorMessageViewModel
|
||||
import com.isolaatti.databinding.FragmentImagesBinding
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.image_list.presentation.ImageAdapterItem
|
||||
import com.isolaatti.images.image_list.presentation.ImageListViewModel
|
||||
import com.isolaatti.images.image_list.presentation.ImagesAdapter
|
||||
import com.isolaatti.images.image_maker.ui.ImageMakerContract
|
||||
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ImagesFragment : Fragment() {
|
||||
lateinit var viewBinding: FragmentImagesBinding
|
||||
lateinit var adapter: ImagesAdapter
|
||||
private val viewModel: ImageListViewModel by viewModels()
|
||||
private val errorViewModel: ErrorMessageViewModel by activityViewModels()
|
||||
private val arguments: ImagesFragmentArgs by navArgs()
|
||||
|
||||
private var cameraPhotoUri: Uri? = null
|
||||
|
||||
private val imageOnClick: (images: List<Image>, position: Int) -> Unit = { images, position ->
|
||||
PictureViewerActivity.startActivityWithImages(requireContext(), images.toTypedArray(), position)
|
||||
}
|
||||
|
||||
private val imageMakerLauncher = registerForActivityResult(ImageMakerContract()) { image ->
|
||||
image?.also {
|
||||
viewModel.lastEvent = ImageListViewModel.Event.ADDED_IMAGE_BEGINNING
|
||||
viewModel.addImageAtTheBeginning(it)
|
||||
}
|
||||
}
|
||||
|
||||
private val choosePictureLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
|
||||
if(it != null) {
|
||||
imageMakerLauncher.launch(it)
|
||||
}
|
||||
}
|
||||
|
||||
private val takePhotoLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) {
|
||||
if(it && cameraPhotoUri != null) {
|
||||
imageMakerLauncher.launch(cameraPhotoUri)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun makePhotoUri(): Uri {
|
||||
val cacheFile = File(requireContext().filesDir, "temp_picture_${Calendar.getInstance().timeInMillis}")
|
||||
return FileProvider.getUriForFile(requireContext(), "${MyApplication.myApp.packageName}.provider", cacheFile)
|
||||
}
|
||||
|
||||
private var deletingImagesDialog: Dialog? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
viewBinding = FragmentImagesBinding.inflate(inflater)
|
||||
|
||||
return viewBinding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
when(arguments.source) {
|
||||
SOURCE_SQUAD -> {}
|
||||
SOURCE_PROFILE -> {
|
||||
viewModel.userId = arguments.sourceId.toInt()
|
||||
viewModel.getUserId()
|
||||
viewModel.loadNext()
|
||||
}
|
||||
}
|
||||
setupAdapter()
|
||||
setupObservers()
|
||||
setupListeners()
|
||||
|
||||
viewBinding.topAppBar.inflateMenu(R.menu.images_menu)
|
||||
}
|
||||
|
||||
private fun showDeleteDialog() {
|
||||
val imagesToDelete = adapter.getSelectedImages()
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.delete)
|
||||
.setMessage(getString(R.string.delete_images_dialog_message, imagesToDelete.size))
|
||||
.setPositiveButton(R.string.yes_continue) { _, _ ->
|
||||
viewModel.removeImages(imagesToDelete)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
private val contextBarCallback: ActionMode.Callback = object: ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
requireActivity().menuInflater.inflate(R.menu.images_context_menu, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||
return when(item?.itemId) {
|
||||
R.id.delete_item -> {
|
||||
showDeleteDialog()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
adapter.deleteMode = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
viewBinding.topAppBar.setNavigationOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
viewBinding.topAppBar.setOnMenuItemClickListener {
|
||||
when(it.itemId) {
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
viewBinding.newPictureButton.setOnClickListener {
|
||||
val popup = PopupMenu(requireContext(), it)
|
||||
popup.menuInflater.inflate(R.menu.add_picture_menu, popup.menu)
|
||||
|
||||
popup.setOnMenuItemClickListener {
|
||||
when(it.itemId) {
|
||||
R.id.take_a_photo_menu_item -> {
|
||||
cameraPhotoUri = makePhotoUri()
|
||||
takePhotoLauncher.launch(cameraPhotoUri)
|
||||
true
|
||||
}
|
||||
R.id.upload_a_picture_item -> {
|
||||
choosePictureLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
popup.show()
|
||||
}
|
||||
viewBinding.swipeToRefresh.setOnRefreshListener {
|
||||
viewModel.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAdapter() {
|
||||
adapter = ImagesAdapter(
|
||||
imageOnClick = imageOnClick,
|
||||
itemWidth = Resources.getSystem().displayMetrics.widthPixels/3,
|
||||
onImageSelectedCountUpdate = {
|
||||
actionMode?.title = getString(R.string.selected_images_count, it)
|
||||
actionMode?.menu?.findItem(R.id.delete_item)?.isEnabled = it > 0
|
||||
},
|
||||
onDeleteMode = {
|
||||
if(viewModel.isUserItself.value == false) return@ImagesAdapter
|
||||
adapter.deleteMode = it
|
||||
actionMode = requireActivity().startActionMode(contextBarCallback)
|
||||
},
|
||||
onContentRequested = {
|
||||
adapter.newContentRequestFinished()
|
||||
viewModel.loadNext()
|
||||
})
|
||||
viewBinding.recyclerView.layoutManager =
|
||||
GridLayoutManager(requireContext(), 3, GridLayoutManager.VERTICAL, false)
|
||||
viewBinding.recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
|
||||
viewModel.liveList.observe(viewLifecycleOwner) { list ->
|
||||
if(viewModel.lastEvent == ImageListViewModel.Event.REMOVED_IMAGE || viewModel.lastEvent == ImageListViewModel.Event.ADDED_IMAGE_BEGINNING) {
|
||||
actionMode?.finish()
|
||||
|
||||
}
|
||||
viewBinding.noImagesCard.visibility = if(list.isEmpty()) View.VISIBLE else View.GONE
|
||||
adapter.submitList(list)
|
||||
}
|
||||
|
||||
viewModel.loading.observe(viewLifecycleOwner) {
|
||||
viewBinding.progressBarLoading.visibility = if(it) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
if(!it) {
|
||||
viewBinding.swipeToRefresh.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner) {
|
||||
errorViewModel.error.value = it
|
||||
}
|
||||
|
||||
viewModel.deleting.observe(viewLifecycleOwner) { deleting ->
|
||||
if(deleting) {
|
||||
deletingImagesDialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(R.string.deleting_please_wait)
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
} else {
|
||||
deletingImagesDialog?.dismiss()
|
||||
deletingImagesDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isUserItself.observe(viewLifecycleOwner) {
|
||||
setupUiForUser(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUiForUser(isUserItSelf: Boolean) {
|
||||
viewBinding.newPictureButton.visibility = if(isUserItSelf) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SOURCE_PROFILE = "source_profile"
|
||||
const val SOURCE_SQUAD = "source_squads"
|
||||
}
|
||||
}
|
||||
@ -32,7 +32,6 @@ import com.isolaatti.common.options_bottom_sheet.domain.OptionClicked
|
||||
import com.isolaatti.common.options_bottom_sheet.domain.Options
|
||||
import com.isolaatti.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
|
||||
import com.isolaatti.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
|
||||
import com.isolaatti.images.image_chooser.ui.ImageChooserContract
|
||||
import com.isolaatti.posting.link_creator.presentation.LinkCreatorViewModel
|
||||
import com.isolaatti.posting.link_creator.ui.LinkCreatorFragment
|
||||
import com.isolaatti.profile.ui.ProfileActivity
|
||||
@ -122,14 +121,6 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
|
||||
|
||||
}
|
||||
|
||||
private val imageChooserLauncher = registerForActivityResult(ImageChooserContract()) { image ->
|
||||
|
||||
|
||||
if(image != null) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun setObservers() {
|
||||
viewModel.comments.observe(viewLifecycleOwner, commentsObserver)
|
||||
viewModel.noMoreContent.observe(viewLifecycleOwner, noMoreContentObserver)
|
||||
@ -180,7 +171,7 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
|
||||
}
|
||||
|
||||
private fun insertImage() {
|
||||
imageChooserLauncher.launch(ImageChooserContract.Requester.UserPost)
|
||||
|
||||
}
|
||||
|
||||
private fun insertLink() {
|
||||
|
||||
@ -7,9 +7,6 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.audio.common.domain.UploadAudioUC
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.audio.drafts.domain.repository.AudioDraftsRepository
|
||||
import com.isolaatti.posting.posts.data.remote.CreatePostDto
|
||||
import com.isolaatti.posting.posts.data.remote.EditPostDto
|
||||
import com.isolaatti.posting.posts.data.remote.EditPostDto.Companion.PRIVACY_ISOLAATTI
|
||||
@ -34,9 +31,7 @@ import javax.inject.Inject
|
||||
class CreatePostViewModel @Inject constructor(
|
||||
private val makePost: MakePost,
|
||||
private val editPost: EditPost,
|
||||
private val loadPost: LoadSinglePost,
|
||||
private val audioDraftsRepository: AudioDraftsRepository,
|
||||
private val uploadAudioUC: UploadAudioUC
|
||||
private val loadPost: LoadSinglePost
|
||||
) : ViewModel() {
|
||||
|
||||
companion object {
|
||||
@ -89,21 +84,7 @@ class CreatePostViewModel @Inject constructor(
|
||||
viewModelScope.launch {
|
||||
|
||||
if(audioDraft != null) {
|
||||
uploadAudioUC(audioDraft!!).onEach { upLoadAudioResource ->
|
||||
Log.d(LOG_TAG, upLoadAudioResource.toString())
|
||||
when(upLoadAudioResource) {
|
||||
is Resource.Success -> {
|
||||
Log.d(LOG_TAG, "uploadAudioResource: Success")
|
||||
audioId = upLoadAudioResource.data?.id
|
||||
sendDiscussion()
|
||||
}
|
||||
|
||||
is Resource.Error -> {}
|
||||
is Resource.Loading -> {
|
||||
sendingPost.postValue(true)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
} else {
|
||||
sendDiscussion()
|
||||
}
|
||||
@ -134,19 +115,7 @@ class CreatePostViewModel @Inject constructor(
|
||||
|
||||
viewModelScope.launch {
|
||||
if(audioDraft != null) {
|
||||
uploadAudioUC(audioDraft!!).onEach { upLoadAudioResource ->
|
||||
when(upLoadAudioResource) {
|
||||
is Resource.Success -> {
|
||||
audioId = upLoadAudioResource.data?.id
|
||||
sendEditDiscussion(postId)
|
||||
}
|
||||
|
||||
is Resource.Error -> {}
|
||||
is Resource.Loading -> {
|
||||
sendingPost.postValue(true)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
} else {
|
||||
sendEditDiscussion(postId)
|
||||
}
|
||||
@ -166,22 +135,6 @@ class CreatePostViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// call this when user has recorded or selected a draft
|
||||
fun putAudioDraft(draft: AudioDraft) {
|
||||
viewModelScope.launch {
|
||||
audioDraftsRepository.getAudioDraftById(draft.id).onEach { draft ->
|
||||
when(draft) {
|
||||
is Resource.Error -> {}
|
||||
is Resource.Loading -> {}
|
||||
is Resource.Success -> {
|
||||
audioAttachment.postValue(draft.data)
|
||||
this@CreatePostViewModel.audioDraft = draft.data!!.id
|
||||
}
|
||||
}
|
||||
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
// call this when user selects an existing audio (not a draft)
|
||||
fun putAudio(audio: Audio) {
|
||||
|
||||
@ -26,16 +26,11 @@ import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.isolaatti.MyApplication
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.audio_selector.ui.AudioSelectorContract
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.audio.drafts.domain.AudioDraft
|
||||
import com.isolaatti.audio.player.AudioPlayerConnector
|
||||
import com.isolaatti.audio.recorder.ui.AudioRecorderContract
|
||||
import com.isolaatti.common.IsolaattiTheme
|
||||
import com.isolaatti.databinding.FragmentMarkdownEditingBinding
|
||||
import com.isolaatti.images.common.components.ImagesRow
|
||||
import com.isolaatti.images.image_chooser.ui.ImageChooserContract
|
||||
import com.isolaatti.posting.link_creator.presentation.LinkCreatorViewModel
|
||||
import com.isolaatti.posting.link_creator.ui.LinkCreatorFragment
|
||||
import com.isolaatti.posting.posts.presentation.CreatePostViewModel
|
||||
@ -52,28 +47,6 @@ class PostEditingFragment : Fragment(){
|
||||
private val viewModel: CreatePostViewModel by activityViewModels()
|
||||
private val linkCreatorViewModel: LinkCreatorViewModel by viewModels()
|
||||
|
||||
private var audioPlayerConnector: AudioPlayerConnector? = null
|
||||
|
||||
private val audioRecorderLauncher = registerForActivityResult(AudioRecorderContract()) { audioDraft ->
|
||||
if(audioDraft != null) {
|
||||
viewModel.putAudioDraft(audioDraft)
|
||||
binding.viewAnimator.displayedChild = 1
|
||||
}
|
||||
}
|
||||
|
||||
private val imageChooserLauncher = registerForActivityResult(ImageChooserContract()) { image ->
|
||||
|
||||
|
||||
if(image != null) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private val audioSelectorLauncher = registerForActivityResult(AudioSelectorContract()) {
|
||||
|
||||
}
|
||||
|
||||
private val choosePictureLauncher = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) {
|
||||
if(it != null) {
|
||||
@ -94,32 +67,9 @@ class PostEditingFragment : Fragment(){
|
||||
}
|
||||
|
||||
}
|
||||
private val audioListener = object: AudioPlayerConnector.Listener {
|
||||
override fun durationChanged(duration: Int, audio: Playable) {
|
||||
binding.audioItem.audioProgress.max = duration
|
||||
}
|
||||
|
||||
override fun onEnded(audio: Playable) {
|
||||
binding.audioItem.audioProgress.progress = 0
|
||||
}
|
||||
|
||||
override fun progressChanged(second: Int, audio: Playable) {
|
||||
binding.audioItem.audioProgress.progress = second
|
||||
}
|
||||
|
||||
override fun onPlaying(isPlaying: Boolean, audio: Playable) {
|
||||
binding.audioItem.playButton.icon = AppCompatResources.getDrawable(requireContext(), if(isPlaying) R.drawable.baseline_pause_circle_24 else R.drawable.baseline_play_circle_24)
|
||||
}
|
||||
|
||||
override fun isLoading(isLoading: Boolean, audio: Playable) {
|
||||
binding.audioItem.audioProgress.isIndeterminate = isLoading
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
audioPlayerConnector = AudioPlayerConnector(requireContext())
|
||||
audioPlayerConnector?.addListener(audioListener)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
@ -135,7 +85,6 @@ class PostEditingFragment : Fragment(){
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewLifecycleOwner.lifecycle.addObserver(audioPlayerConnector!!)
|
||||
|
||||
viewLifecycleOwner.lifecycle.addObserver(object: LifecycleEventObserver {
|
||||
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||
@ -146,7 +95,7 @@ class PostEditingFragment : Fragment(){
|
||||
setupListeners()
|
||||
setupObservers()
|
||||
|
||||
binding.imagesRowCompose.apply {
|
||||
binding.composeView.apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
val pictures by viewModel.photos.collectAsState()
|
||||
@ -156,7 +105,9 @@ class PostEditingFragment : Fragment(){
|
||||
images = pictures,
|
||||
deletable = true,
|
||||
addable = true,
|
||||
onClick = {},
|
||||
onClick = {
|
||||
|
||||
},
|
||||
onDeleteClick = {
|
||||
viewModel.removePicture(it)
|
||||
},
|
||||
@ -175,7 +126,6 @@ class PostEditingFragment : Fragment(){
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
viewLifecycleOwner.lifecycle.removeObserver(audioPlayerConnector!!)
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
@ -186,34 +136,6 @@ class PostEditingFragment : Fragment(){
|
||||
viewModel.validation.postValue(!text.isNullOrEmpty())
|
||||
viewModel.content = text.toString()
|
||||
}
|
||||
|
||||
binding.addAudioButton.setOnClickListener {
|
||||
|
||||
val popupMenu = PopupMenu(requireContext(), it)
|
||||
|
||||
popupMenu.inflate(R.menu.attach_audio_menu)
|
||||
|
||||
popupMenu.setOnMenuItemClickListener { menuItem ->
|
||||
when(menuItem.itemId){
|
||||
R.id.record_new_audio_item -> {
|
||||
audioRecorderLauncher.launch(null)
|
||||
true
|
||||
}
|
||||
R.id.select_from_audios -> {
|
||||
audioSelectorLauncher.launch(AudioSelectorContract.SelectorConfig(false, null))
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
binding.removeAudio.setOnClickListener {
|
||||
binding.viewAnimator.displayedChild = 0
|
||||
viewModel.removeAudio()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObservers(){
|
||||
@ -229,30 +151,7 @@ class PostEditingFragment : Fragment(){
|
||||
}
|
||||
|
||||
viewModel.audioAttachment.observe(viewLifecycleOwner) { playable ->
|
||||
if(playable != null) {
|
||||
audioPlayerConnector?.stopPlayback()
|
||||
binding.audioItem.playButton.setOnClickListener {
|
||||
audioPlayerConnector?.playPauseAudio(playable)
|
||||
}
|
||||
|
||||
when(playable) {
|
||||
is Audio -> {
|
||||
binding.audioItem.textViewDescription.text = playable.name
|
||||
}
|
||||
is AudioDraft -> {
|
||||
binding.audioItem.textViewDescription.text = playable.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun insertImage() {
|
||||
imageChooserLauncher.launch(ImageChooserContract.Requester.UserPost)
|
||||
}
|
||||
|
||||
private fun insertLink() {
|
||||
LinkCreatorFragment().show(childFragmentManager, null)
|
||||
}
|
||||
}
|
||||
@ -15,9 +15,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.isolaatti.BuildConfig
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.audio.player.AudioPlayerConnector
|
||||
import com.isolaatti.common.CoilImageLoader
|
||||
import com.isolaatti.common.Dialogs
|
||||
import com.isolaatti.common.ErrorMessageViewModel
|
||||
import com.isolaatti.common.OnUserInteractedWithPostCallback
|
||||
@ -39,12 +36,6 @@ import com.isolaatti.profile.ui.ProfileActivity
|
||||
import com.isolaatti.reports.data.ContentType
|
||||
import com.isolaatti.reports.ui.NewReportBottomSheetDialogFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.noties.markwon.AbstractMarkwonPlugin
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.MarkwonConfiguration
|
||||
import io.noties.markwon.image.coil.CoilImagesPlugin
|
||||
import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute
|
||||
import io.noties.markwon.linkify.LinkifyPlugin
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
|
||||
@ -60,35 +51,6 @@ class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
|
||||
private val viewModel: PostListingViewModel by viewModels()
|
||||
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
|
||||
private lateinit var adapter: PostsRecyclerViewAdapter
|
||||
private lateinit var audioPlayerConnector: AudioPlayerConnector
|
||||
|
||||
private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener {
|
||||
override fun onPlaying(isPlaying: Boolean, audio: Playable) {
|
||||
if(audio is Audio)
|
||||
adapter.setIsPlaying(isPlaying, audio)
|
||||
}
|
||||
|
||||
override fun isLoading(isLoading: Boolean, audio: Playable) {
|
||||
if(audio is Audio)
|
||||
adapter.setIsLoading(isLoading, audio)
|
||||
}
|
||||
|
||||
override fun progressChanged(second: Int, audio: Playable) {
|
||||
if(audio is Audio)
|
||||
adapter.setProgress(second, audio)
|
||||
}
|
||||
|
||||
override fun durationChanged(duration: Int, audio: Playable) {
|
||||
if(audio is Audio)
|
||||
adapter.setDuration(duration, audio)
|
||||
}
|
||||
|
||||
override fun onEnded(audio: Playable) {
|
||||
if(audio is Audio)
|
||||
adapter.setEnded(audio)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -109,22 +71,6 @@ class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
audioPlayerConnector = AudioPlayerConnector(requireContext())
|
||||
audioPlayerConnector.addListener(audioPlayerConnectorListener)
|
||||
viewLifecycleOwner.lifecycle.addObserver(audioPlayerConnector)
|
||||
|
||||
val markwon = Markwon.builder(requireContext())
|
||||
.usePlugin(object: AbstractMarkwonPlugin() {
|
||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
||||
builder
|
||||
.imageDestinationProcessor(
|
||||
ImageDestinationProcessorRelativeToAbsolute
|
||||
.create(BuildConfig.backend))
|
||||
}
|
||||
})
|
||||
.usePlugin(CoilImagesPlugin.create(requireContext(), CoilImageLoader.imageLoader))
|
||||
.usePlugin(LinkifyPlugin.create())
|
||||
.build()
|
||||
adapter = PostsRecyclerViewAdapter(this)
|
||||
viewBinding.feedRecyclerView.adapter = adapter
|
||||
viewBinding.feedRecyclerView.setItemViewCacheSize(7)
|
||||
@ -177,7 +123,6 @@ class PostListingFragment : Fragment(), OnUserInteractedWithPostCallback {
|
||||
}
|
||||
|
||||
override fun onPlay(audio: Audio) {
|
||||
audioPlayerConnector.playPauseAudio(audio)
|
||||
}
|
||||
|
||||
override fun onMoreInfo(postId: Long) {
|
||||
|
||||
@ -22,11 +22,8 @@ import coil.load
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.isolaatti.BuildConfig
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.audio.audio_selector.ui.AudioSelectorContract
|
||||
import com.isolaatti.audio.audios_list.ui.AudiosFragment
|
||||
import com.isolaatti.audio.common.domain.Audio
|
||||
import com.isolaatti.audio.common.domain.Playable
|
||||
import com.isolaatti.audio.player.AudioPlayerConnector
|
||||
import com.isolaatti.common.CoilImageLoader.imageLoader
|
||||
import com.isolaatti.common.Dialogs
|
||||
import com.isolaatti.common.ErrorMessageViewModel
|
||||
@ -39,8 +36,6 @@ import com.isolaatti.databinding.FragmentDiscussionsBinding
|
||||
import com.isolaatti.followers.domain.FollowingState
|
||||
import com.isolaatti.hashtags.ui.HashtagsPostsActivity
|
||||
import com.isolaatti.images.common.domain.entity.Image
|
||||
import com.isolaatti.images.image_chooser.ui.ImageChooserContract
|
||||
import com.isolaatti.images.image_list.ui.ImagesFragment
|
||||
import com.isolaatti.images.picture_viewer.ui.PictureViewerActivity
|
||||
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
|
||||
import com.isolaatti.posting.posts.domain.entity.Post
|
||||
@ -78,7 +73,6 @@ class ProfileMainFragment : Fragment() {
|
||||
|
||||
private var audioDescriptionAudio: Audio? = null
|
||||
|
||||
private lateinit var audioPlayerConnector: AudioPlayerConnector
|
||||
|
||||
// collapsing bar
|
||||
private var title = ""
|
||||
@ -94,12 +88,6 @@ class ProfileMainFragment : Fragment() {
|
||||
viewModel.onPostAddedAtTheBeginning(it)
|
||||
}
|
||||
|
||||
private val chooseImageLauncher = registerForActivityResult(ImageChooserContract()) { image ->
|
||||
// here change profile picture
|
||||
if(image != null) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private val editDiscussion = registerForActivityResult(EditPostContract()) {
|
||||
if(it != null) {
|
||||
@ -113,64 +101,6 @@ class ProfileMainFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private val audioSelectorLauncher = registerForActivityResult(AudioSelectorContract()) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
private val audioPlayerConnectorListener = object: AudioPlayerConnector.Listener {
|
||||
override fun onPlaying(isPlaying: Boolean, audio: Playable) {
|
||||
Log.d(LOG_TAG, "onPlaying() isPlaying: $isPlaying: audio $audio")
|
||||
if(audio == audioDescriptionAudio) {
|
||||
viewBinding.playButton.icon =
|
||||
AppCompatResources.getDrawable(
|
||||
requireContext(),
|
||||
if(isPlaying) R.drawable.baseline_pause_circle_24 else R.drawable.baseline_play_circle_24
|
||||
)
|
||||
}
|
||||
if(audio is Audio)
|
||||
postsAdapter.setIsPlaying(isPlaying, audio)
|
||||
}
|
||||
|
||||
override fun isLoading(isLoading: Boolean, audio: Playable) {
|
||||
if(audio == audioDescriptionAudio) {
|
||||
viewBinding.playButton.isEnabled = !isLoading
|
||||
viewBinding.audioProgress.isIndeterminate = isLoading
|
||||
}
|
||||
|
||||
if(audio is Audio)
|
||||
postsAdapter.setIsLoading(isLoading, audio)
|
||||
|
||||
}
|
||||
|
||||
override fun progressChanged(second: Int, audio: Playable) {
|
||||
if(audio == audioDescriptionAudio) {
|
||||
viewBinding.audioProgress.setProgress(second, true)
|
||||
}
|
||||
|
||||
if(audio is Audio)
|
||||
postsAdapter.setProgress(second, audio)
|
||||
}
|
||||
|
||||
override fun durationChanged(duration: Int, audio: Playable) {
|
||||
if(audio == audioDescriptionAudio) {
|
||||
viewBinding.audioProgress.max = duration
|
||||
}
|
||||
|
||||
if(audio is Audio)
|
||||
postsAdapter.setDuration(duration, audio)
|
||||
}
|
||||
|
||||
override fun onEnded(audio: Playable) {
|
||||
if(audio == audioDescriptionAudio) {
|
||||
viewBinding.audioProgress.progress = 0
|
||||
}
|
||||
|
||||
if(audio is Audio)
|
||||
postsAdapter.setEnded(audio)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val profileObserver = Observer<UserProfile> { profile ->
|
||||
viewBinding.profileImageView.load(UrlGen.userProfileImage(profile.userId, invalidateCache = true), imageLoader)
|
||||
@ -243,7 +173,7 @@ class ProfileMainFragment : Fragment() {
|
||||
val profile = optionClicked.payload as? UserProfile
|
||||
when(optionClicked.optionId) {
|
||||
Options.Option.OPTION_PROFILE_PHOTO_CHANGE_PHOTO -> {
|
||||
chooseImageLauncher.launch(ImageChooserContract.Requester.UserPost)
|
||||
|
||||
}
|
||||
Options.Option.OPTION_PROFILE_PHOTO_REMOVE_PHOTO -> {
|
||||
showRemoveProfileImageDialog()
|
||||
@ -287,7 +217,6 @@ class ProfileMainFragment : Fragment() {
|
||||
}
|
||||
Options.PROFILE_DESCRIPTION_OPTIONS -> {
|
||||
optionsViewModel.handle()
|
||||
audioSelectorLauncher.launch(AudioSelectorContract.SelectorConfig(false, null))
|
||||
}
|
||||
}
|
||||
|
||||
@ -394,7 +323,7 @@ class ProfileMainFragment : Fragment() {
|
||||
}
|
||||
viewBinding.playButton.setOnClickListener {
|
||||
audioDescriptionAudio?.let { audio ->
|
||||
audioPlayerConnector.playPauseAudio(audio)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -499,7 +428,7 @@ class ProfileMainFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onPlay(audio: Audio) {
|
||||
audioPlayerConnector.playPauseAudio(audio)
|
||||
|
||||
}
|
||||
|
||||
override fun onLoadMore() {
|
||||
@ -543,12 +472,6 @@ class ProfileMainFragment : Fragment() {
|
||||
setObservers()
|
||||
setupCollapsingBar()
|
||||
|
||||
audioPlayerConnector = AudioPlayerConnector(requireContext())
|
||||
|
||||
audioPlayerConnector.addListener(audioPlayerConnectorListener)
|
||||
|
||||
viewLifecycleOwner.lifecycle.addObserver(audioPlayerConnector)
|
||||
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
|
||||
@ -16,39 +16,6 @@
|
||||
app:navigationIcon="@drawable/baseline_close_24"
|
||||
app:menu="@menu/audio_recorder_menu"/>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/input_draft_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
android:hint="Name"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/materialCardView2"
|
||||
style="?attr/materialCardViewElevatedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="24dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/time"
|
||||
app:layout_constraintTop_toBottomOf="@id/input_draft_name"
|
||||
tools:layout_editor_absoluteX="24dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/time"
|
||||
|
||||
@ -7,15 +7,15 @@
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:fitsSystemWindows="true">
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
@ -64,6 +64,7 @@
|
||||
android:layout_marginBottom="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:srcCompat="@drawable/baseline_send_24" />
|
||||
app:srcCompat="@drawable/baseline_send_24"
|
||||
android:fitsSystemWindows="true"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@ -25,55 +25,9 @@
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/images_row_compose"
|
||||
android:id="@+id/compose_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="160dp"/>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="30dp"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
style="?attr/materialCardViewFilledStyle">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_margin="16dp">
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/audio_attachment"/>
|
||||
<ViewAnimator
|
||||
android:id="@+id/view_animator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp">
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/add_audio_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:text="Attach audio" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<include android:id="@+id/audio_item" layout="@layout/audio_attachment"/>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/remove_audio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/audio_item"
|
||||
android:layout_marginTop="4dp"
|
||||
style="?attr/materialIconButtonFilledTonalStyle"
|
||||
app:icon="@drawable/baseline_close_24"
|
||||
android:text="@string/remove"/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ViewAnimator>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@ -8,12 +8,6 @@
|
||||
android:id="@+id/mainFragment"
|
||||
android:name="com.isolaatti.profile.ui.ProfileMainFragment"
|
||||
android:label="DiscussionsFragment" >
|
||||
<action
|
||||
android:id="@+id/action_discussionsFragment_to_audiosFragment"
|
||||
app:destination="@id/audiosFragment" />
|
||||
<action
|
||||
android:id="@+id/action_discussionsFragment_to_imagesFragment"
|
||||
app:destination="@id/imagesFragment" />
|
||||
<action
|
||||
android:id="@+id/action_discussionsFragment_to_blockProfileFragment"
|
||||
app:destination="@id/blockProfileFragment" />
|
||||
@ -33,27 +27,6 @@
|
||||
app:destination="@id/qrCodeFragment"
|
||||
app:popExitAnim="@android:anim/slide_out_right" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/audiosFragment"
|
||||
android:name="com.isolaatti.audio.audios_list.ui.AudiosFragment"
|
||||
android:label="AudiosFragment" >
|
||||
<argument
|
||||
android:name="source"
|
||||
app:argType="string" />
|
||||
<argument android:name="sourceId"
|
||||
app:argType="string" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/imagesFragment"
|
||||
android:name="com.isolaatti.images.image_list.ui.ImagesFragment"
|
||||
android:label="ImagesFragment" >
|
||||
<argument
|
||||
android:name="source"
|
||||
app:argType="string" />
|
||||
<argument
|
||||
android:name="sourceId"
|
||||
app:argType="string" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/blockProfileFragment"
|
||||
android:name="com.isolaatti.profile.ui.BlockProfileFragment"
|
||||
|
||||
14
build.gradle
14
build.gradle
@ -14,10 +14,12 @@ buildscript {
|
||||
}
|
||||
}
|
||||
plugins {
|
||||
id 'com.android.application' version '8.3.1' apply false
|
||||
id 'com.android.library' version '8.3.1' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
||||
id 'com.google.dagger.hilt.android' version '2.47' apply false
|
||||
id 'com.google.gms.google-services' version '4.4.0' apply false
|
||||
id 'com.google.firebase.crashlytics' version '2.9.9' apply false
|
||||
id 'com.android.application' version '8.7.3' apply false
|
||||
id 'com.android.library' version '8.7.3' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '2.1.0' apply false
|
||||
id 'com.google.dagger.hilt.android' version '2.53.1' apply false
|
||||
id 'com.google.gms.google-services' version '4.4.2' apply false
|
||||
id 'com.google.firebase.crashlytics' version '3.0.2' apply false
|
||||
id 'org.jetbrains.kotlin.kapt' version '2.1.0' apply false
|
||||
id 'com.google.devtools.ksp' version '2.1.0-1.0.29' apply false
|
||||
}
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Thu Jul 27 21:16:21 CST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user