Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
7770997fd4
|
|||
|
f45ff8963d
|
|||
|
5988cbf1fb
|
|||
| 13654cde70 | |||
| 9064dbe2ef | |||
|
0537da79e4
|
|||
|
4804049274
|
|||
|
8db6ed0e82
|
|||
|
8b9901383a
|
|||
|
cf2adeef7a
|
|||
|
a7481135b4
|
|||
|
748a23e1c0
|
|||
|
f078cfc6e1
|
|||
|
8bb1f422c1
|
|||
|
327dfba425
|
|||
|
96759e402d
|
|||
|
ed76fb2fb2
|
|||
|
870278f240
|
|||
|
4eef77bd3b
|
|||
|
2d957db948
|
|||
| 22bed6a961 | |||
|
b443c18a19
|
|||
|
89f1e350b3
|
|||
|
0f976f685f
|
|||
|
c07186a7df
|
|||
|
15a5e217a5
|
|||
|
b86ab591fe
|
|||
|
70c85d159e
|
|||
|
d6c5e937df
|
|||
|
829bbbff7a
|
|||
|
e1ebf412bd
|
|||
|
5c133b655e
|
|||
|
cc1edbc65c
|
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
release/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Log/OS Files
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Android Studio generated files and folders
|
||||||
|
captures/
|
||||||
|
.externalNativeBuild/
|
||||||
|
.cxx/
|
||||||
|
*.aab
|
||||||
|
*.apk
|
||||||
|
output-metadata.json
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
*.iml
|
||||||
|
.idea/
|
||||||
|
misc.xml
|
||||||
|
deploymentTargetDropDown.xml
|
||||||
|
render.experimental.xml
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
*.jks
|
||||||
|
*.keystore
|
||||||
|
|
||||||
|
# Google Services (e.g. APIs or Firebase)
|
||||||
|
google-services.json
|
||||||
|
|
||||||
|
# Android Profiling
|
||||||
|
*.hprof
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,769 +0,0 @@
|
|||||||
package org.gradle.accessors.dm;
|
|
||||||
|
|
||||||
import org.gradle.api.NonNullApi;
|
|
||||||
import org.gradle.api.artifacts.MinimalExternalModuleDependency;
|
|
||||||
import org.gradle.plugin.use.PluginDependency;
|
|
||||||
import org.gradle.api.artifacts.ExternalModuleDependencyBundle;
|
|
||||||
import org.gradle.api.artifacts.MutableVersionConstraint;
|
|
||||||
import org.gradle.api.provider.Provider;
|
|
||||||
import org.gradle.api.model.ObjectFactory;
|
|
||||||
import org.gradle.api.provider.ProviderFactory;
|
|
||||||
import org.gradle.api.internal.catalog.AbstractExternalDependencyFactory;
|
|
||||||
import org.gradle.api.internal.catalog.DefaultVersionCatalog;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.gradle.api.internal.attributes.ImmutableAttributesFactory;
|
|
||||||
import org.gradle.api.internal.artifacts.dsl.CapabilityNotationParser;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A catalog of dependencies accessible via the {@code libs} extension.
|
|
||||||
*/
|
|
||||||
@NonNullApi
|
|
||||||
public class LibrariesForLibs extends AbstractExternalDependencyFactory {
|
|
||||||
|
|
||||||
private final AbstractExternalDependencyFactory owner = this;
|
|
||||||
private final AndroidxLibraryAccessors laccForAndroidxLibraryAccessors = new AndroidxLibraryAccessors(owner);
|
|
||||||
private final CoilLibraryAccessors laccForCoilLibraryAccessors = new CoilLibraryAccessors(owner);
|
|
||||||
private final KotlinxLibraryAccessors laccForKotlinxLibraryAccessors = new KotlinxLibraryAccessors(owner);
|
|
||||||
private final VersionAccessors vaccForVersionAccessors = new VersionAccessors(providers, config);
|
|
||||||
private final BundleAccessors baccForBundleAccessors = new BundleAccessors(objects, providers, config, attributesFactory, capabilityNotationParser);
|
|
||||||
private final PluginAccessors paccForPluginAccessors = new PluginAccessors(providers, config);
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public LibrariesForLibs(DefaultVersionCatalog config, ProviderFactory providers, ObjectFactory objects, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) {
|
|
||||||
super(config, providers, objects, attributesFactory, capabilityNotationParser);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>junit</b> with <b>junit:junit</b> coordinates and
|
|
||||||
* with version reference <b>junit</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getJunit() {
|
|
||||||
return create("junit");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>mpandroidchart</b> with <b>com.github.PhilJay:MPAndroidChart</b> coordinates and
|
|
||||||
* with version <b>v3.1.0</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getMpandroidchart() {
|
|
||||||
return create("mpandroidchart");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx</b>
|
|
||||||
*/
|
|
||||||
public AndroidxLibraryAccessors getAndroidx() {
|
|
||||||
return laccForAndroidxLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>coil</b>
|
|
||||||
*/
|
|
||||||
public CoilLibraryAccessors getCoil() {
|
|
||||||
return laccForCoilLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>kotlinx</b>
|
|
||||||
*/
|
|
||||||
public KotlinxLibraryAccessors getKotlinx() {
|
|
||||||
return laccForKotlinxLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of versions at <b>versions</b>
|
|
||||||
*/
|
|
||||||
public VersionAccessors getVersions() {
|
|
||||||
return vaccForVersionAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of bundles at <b>bundles</b>
|
|
||||||
*/
|
|
||||||
public BundleAccessors getBundles() {
|
|
||||||
return baccForBundleAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of plugins at <b>plugins</b>
|
|
||||||
*/
|
|
||||||
public PluginAccessors getPlugins() {
|
|
||||||
return paccForPluginAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxLibraryAccessors extends SubDependencyFactory {
|
|
||||||
private final AndroidxActivityLibraryAccessors laccForAndroidxActivityLibraryAccessors = new AndroidxActivityLibraryAccessors(owner);
|
|
||||||
private final AndroidxComposeLibraryAccessors laccForAndroidxComposeLibraryAccessors = new AndroidxComposeLibraryAccessors(owner);
|
|
||||||
private final AndroidxCoreLibraryAccessors laccForAndroidxCoreLibraryAccessors = new AndroidxCoreLibraryAccessors(owner);
|
|
||||||
private final AndroidxEspressoLibraryAccessors laccForAndroidxEspressoLibraryAccessors = new AndroidxEspressoLibraryAccessors(owner);
|
|
||||||
private final AndroidxLifecycleLibraryAccessors laccForAndroidxLifecycleLibraryAccessors = new AndroidxLifecycleLibraryAccessors(owner);
|
|
||||||
private final AndroidxNavigationLibraryAccessors laccForAndroidxNavigationLibraryAccessors = new AndroidxNavigationLibraryAccessors(owner);
|
|
||||||
private final AndroidxRoomLibraryAccessors laccForAndroidxRoomLibraryAccessors = new AndroidxRoomLibraryAccessors(owner);
|
|
||||||
private final AndroidxUiLibraryAccessors laccForAndroidxUiLibraryAccessors = new AndroidxUiLibraryAccessors(owner);
|
|
||||||
|
|
||||||
public AndroidxLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>junit</b> with <b>androidx.test.ext:junit</b> coordinates and
|
|
||||||
* with version reference <b>junitVersion</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getJunit() {
|
|
||||||
return create("androidx.junit");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>material3</b> with <b>androidx.compose.material3:material3</b> coordinates and
|
|
||||||
* with <b>no version specified</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getMaterial3() {
|
|
||||||
return create("androidx.material3");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.activity</b>
|
|
||||||
*/
|
|
||||||
public AndroidxActivityLibraryAccessors getActivity() {
|
|
||||||
return laccForAndroidxActivityLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.compose</b>
|
|
||||||
*/
|
|
||||||
public AndroidxComposeLibraryAccessors getCompose() {
|
|
||||||
return laccForAndroidxComposeLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.core</b>
|
|
||||||
*/
|
|
||||||
public AndroidxCoreLibraryAccessors getCore() {
|
|
||||||
return laccForAndroidxCoreLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.espresso</b>
|
|
||||||
*/
|
|
||||||
public AndroidxEspressoLibraryAccessors getEspresso() {
|
|
||||||
return laccForAndroidxEspressoLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.lifecycle</b>
|
|
||||||
*/
|
|
||||||
public AndroidxLifecycleLibraryAccessors getLifecycle() {
|
|
||||||
return laccForAndroidxLifecycleLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.navigation</b>
|
|
||||||
*/
|
|
||||||
public AndroidxNavigationLibraryAccessors getNavigation() {
|
|
||||||
return laccForAndroidxNavigationLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.room</b>
|
|
||||||
*/
|
|
||||||
public AndroidxRoomLibraryAccessors getRoom() {
|
|
||||||
return laccForAndroidxRoomLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.ui</b>
|
|
||||||
*/
|
|
||||||
public AndroidxUiLibraryAccessors getUi() {
|
|
||||||
return laccForAndroidxUiLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxActivityLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public AndroidxActivityLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>compose</b> with <b>androidx.activity:activity-compose</b> coordinates and
|
|
||||||
* with version reference <b>activityCompose</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getCompose() {
|
|
||||||
return create("androidx.activity.compose");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxComposeLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public AndroidxComposeLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>bom</b> with <b>androidx.compose:compose-bom</b> coordinates and
|
|
||||||
* with version reference <b>composeBom</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getBom() {
|
|
||||||
return create("androidx.compose.bom");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxCoreLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public AndroidxCoreLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>ktx</b> with <b>androidx.core:core-ktx</b> coordinates and
|
|
||||||
* with version reference <b>coreKtx</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getKtx() {
|
|
||||||
return create("androidx.core.ktx");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxEspressoLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public AndroidxEspressoLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>core</b> with <b>androidx.test.espresso:espresso-core</b> coordinates and
|
|
||||||
* with version reference <b>espressoCore</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getCore() {
|
|
||||||
return create("androidx.espresso.core");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxLifecycleLibraryAccessors extends SubDependencyFactory {
|
|
||||||
private final AndroidxLifecycleRuntimeLibraryAccessors laccForAndroidxLifecycleRuntimeLibraryAccessors = new AndroidxLifecycleRuntimeLibraryAccessors(owner);
|
|
||||||
private final AndroidxLifecycleViewmodelLibraryAccessors laccForAndroidxLifecycleViewmodelLibraryAccessors = new AndroidxLifecycleViewmodelLibraryAccessors(owner);
|
|
||||||
|
|
||||||
public AndroidxLifecycleLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.lifecycle.runtime</b>
|
|
||||||
*/
|
|
||||||
public AndroidxLifecycleRuntimeLibraryAccessors getRuntime() {
|
|
||||||
return laccForAndroidxLifecycleRuntimeLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.lifecycle.viewmodel</b>
|
|
||||||
*/
|
|
||||||
public AndroidxLifecycleViewmodelLibraryAccessors getViewmodel() {
|
|
||||||
return laccForAndroidxLifecycleViewmodelLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxLifecycleRuntimeLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public AndroidxLifecycleRuntimeLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>ktx</b> with <b>androidx.lifecycle:lifecycle-runtime-ktx</b> coordinates and
|
|
||||||
* with version reference <b>lifecycleRuntimeKtx</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getKtx() {
|
|
||||||
return create("androidx.lifecycle.runtime.ktx");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxLifecycleViewmodelLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public AndroidxLifecycleViewmodelLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>compose</b> with <b>androidx.lifecycle:lifecycle-viewmodel-compose</b> coordinates and
|
|
||||||
* with version reference <b>viewmodel</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getCompose() {
|
|
||||||
return create("androidx.lifecycle.viewmodel.compose");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxNavigationLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public AndroidxNavigationLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>compose</b> with <b>androidx.navigation:navigation-compose</b> coordinates and
|
|
||||||
* with version reference <b>navigation</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getCompose() {
|
|
||||||
return create("androidx.navigation.compose");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxRoomLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public AndroidxRoomLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>compiler</b> with <b>androidx.room:room-compiler</b> coordinates and
|
|
||||||
* with version reference <b>room</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getCompiler() {
|
|
||||||
return create("androidx.room.compiler");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>ktx</b> with <b>androidx.room:room-ktx</b> coordinates and
|
|
||||||
* with version reference <b>room</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getKtx() {
|
|
||||||
return create("androidx.room.ktx");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>runtime</b> with <b>androidx.room:room-runtime</b> coordinates and
|
|
||||||
* with version reference <b>room</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getRuntime() {
|
|
||||||
return create("androidx.room.runtime");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxUiLibraryAccessors extends SubDependencyFactory implements DependencyNotationSupplier {
|
|
||||||
private final AndroidxUiTestLibraryAccessors laccForAndroidxUiTestLibraryAccessors = new AndroidxUiTestLibraryAccessors(owner);
|
|
||||||
private final AndroidxUiToolingLibraryAccessors laccForAndroidxUiToolingLibraryAccessors = new AndroidxUiToolingLibraryAccessors(owner);
|
|
||||||
|
|
||||||
public AndroidxUiLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>ui</b> with <b>androidx.compose.ui:ui</b> coordinates and
|
|
||||||
* with <b>no version specified</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> asProvider() {
|
|
||||||
return create("androidx.ui");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>graphics</b> with <b>androidx.compose.ui:ui-graphics</b> coordinates and
|
|
||||||
* with <b>no version specified</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getGraphics() {
|
|
||||||
return create("androidx.ui.graphics");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.ui.test</b>
|
|
||||||
*/
|
|
||||||
public AndroidxUiTestLibraryAccessors getTest() {
|
|
||||||
return laccForAndroidxUiTestLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>androidx.ui.tooling</b>
|
|
||||||
*/
|
|
||||||
public AndroidxUiToolingLibraryAccessors getTooling() {
|
|
||||||
return laccForAndroidxUiToolingLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxUiTestLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public AndroidxUiTestLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>junit4</b> with <b>androidx.compose.ui:ui-test-junit4</b> coordinates and
|
|
||||||
* with <b>no version specified</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getJunit4() {
|
|
||||||
return create("androidx.ui.test.junit4");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>manifest</b> with <b>androidx.compose.ui:ui-test-manifest</b> coordinates and
|
|
||||||
* with <b>no version specified</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getManifest() {
|
|
||||||
return create("androidx.ui.test.manifest");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidxUiToolingLibraryAccessors extends SubDependencyFactory implements DependencyNotationSupplier {
|
|
||||||
|
|
||||||
public AndroidxUiToolingLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>tooling</b> with <b>androidx.compose.ui:ui-tooling</b> coordinates and
|
|
||||||
* with <b>no version specified</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> asProvider() {
|
|
||||||
return create("androidx.ui.tooling");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>preview</b> with <b>androidx.compose.ui:ui-tooling-preview</b> coordinates and
|
|
||||||
* with <b>no version specified</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getPreview() {
|
|
||||||
return create("androidx.ui.tooling.preview");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class CoilLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public CoilLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>compose</b> with <b>io.coil-kt:coil-compose</b> coordinates and
|
|
||||||
* with version reference <b>coil</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getCompose() {
|
|
||||||
return create("coil.compose");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class KotlinxLibraryAccessors extends SubDependencyFactory {
|
|
||||||
private final KotlinxCoroutinesLibraryAccessors laccForKotlinxCoroutinesLibraryAccessors = new KotlinxCoroutinesLibraryAccessors(owner);
|
|
||||||
private final KotlinxSerializationLibraryAccessors laccForKotlinxSerializationLibraryAccessors = new KotlinxSerializationLibraryAccessors(owner);
|
|
||||||
|
|
||||||
public KotlinxLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>kotlinx.coroutines</b>
|
|
||||||
*/
|
|
||||||
public KotlinxCoroutinesLibraryAccessors getCoroutines() {
|
|
||||||
return laccForKotlinxCoroutinesLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of libraries at <b>kotlinx.serialization</b>
|
|
||||||
*/
|
|
||||||
public KotlinxSerializationLibraryAccessors getSerialization() {
|
|
||||||
return laccForKotlinxSerializationLibraryAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class KotlinxCoroutinesLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public KotlinxCoroutinesLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>android</b> with <b>org.jetbrains.kotlinx:kotlinx-coroutines-android</b> coordinates and
|
|
||||||
* with version reference <b>kotlinxCoroutines</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getAndroid() {
|
|
||||||
return create("kotlinx.coroutines.android");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class KotlinxSerializationLibraryAccessors extends SubDependencyFactory {
|
|
||||||
|
|
||||||
public KotlinxSerializationLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dependency provider for <b>json</b> with <b>org.jetbrains.kotlinx:kotlinx-serialization-json</b> coordinates and
|
|
||||||
* with version reference <b>kotlinxSerialization</b>
|
|
||||||
* <p>
|
|
||||||
* This dependency was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<MinimalExternalModuleDependency> getJson() {
|
|
||||||
return create("kotlinx.serialization.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class VersionAccessors extends VersionFactory {
|
|
||||||
|
|
||||||
public VersionAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>activityCompose</b> with value <b>1.10.1</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getActivityCompose() { return getVersion("activityCompose"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>agp</b> with value <b>8.9.1</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getAgp() { return getVersion("agp"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>coil</b> with value <b>2.7.0</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getCoil() { return getVersion("coil"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>composeBom</b> with value <b>2024.09.00</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getComposeBom() { return getVersion("composeBom"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>coreKtx</b> with value <b>1.15.0</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getCoreKtx() { return getVersion("coreKtx"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>espressoCore</b> with value <b>3.7.0</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getEspressoCore() { return getVersion("espressoCore"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>junit</b> with value <b>4.13.2</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getJunit() { return getVersion("junit"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>junitVersion</b> with value <b>1.3.0</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getJunitVersion() { return getVersion("junitVersion"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>kotlin</b> with value <b>2.0.21</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getKotlin() { return getVersion("kotlin"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>kotlinxCoroutines</b> with value <b>1.9.0</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getKotlinxCoroutines() { return getVersion("kotlinxCoroutines"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>kotlinxSerialization</b> with value <b>1.7.1</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getKotlinxSerialization() { return getVersion("kotlinxSerialization"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>ksp</b> with value <b>2.0.21-1.0.25</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getKsp() { return getVersion("ksp"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>lifecycleRuntimeKtx</b> with value <b>2.9.2</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getLifecycleRuntimeKtx() { return getVersion("lifecycleRuntimeKtx"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>navigation</b> with value <b>2.8.4</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getNavigation() { return getVersion("navigation"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>room</b> with value <b>2.6.1</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getRoom() { return getVersion("room"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Version alias <b>viewmodel</b> with value <b>2.9.2</b>
|
|
||||||
* <p>
|
|
||||||
* If the version is a rich version and cannot be represented as a
|
|
||||||
* single version string, an empty string is returned.
|
|
||||||
* <p>
|
|
||||||
* This version was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<String> getViewmodel() { return getVersion("viewmodel"); }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class BundleAccessors extends BundleFactory {
|
|
||||||
|
|
||||||
public BundleAccessors(ObjectFactory objects, ProviderFactory providers, DefaultVersionCatalog config, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) { super(objects, providers, config, attributesFactory, capabilityNotationParser); }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class PluginAccessors extends PluginFactory {
|
|
||||||
private final AndroidPluginAccessors paccForAndroidPluginAccessors = new AndroidPluginAccessors(providers, config);
|
|
||||||
private final KotlinPluginAccessors paccForKotlinPluginAccessors = new KotlinPluginAccessors(providers, config);
|
|
||||||
|
|
||||||
public PluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin provider for <b>ksp</b> with plugin id <b>com.google.devtools.ksp</b> and
|
|
||||||
* with version reference <b>ksp</b>
|
|
||||||
* <p>
|
|
||||||
* This plugin was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<PluginDependency> getKsp() { return createPlugin("ksp"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of plugins at <b>plugins.android</b>
|
|
||||||
*/
|
|
||||||
public AndroidPluginAccessors getAndroid() {
|
|
||||||
return paccForAndroidPluginAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group of plugins at <b>plugins.kotlin</b>
|
|
||||||
*/
|
|
||||||
public KotlinPluginAccessors getKotlin() {
|
|
||||||
return paccForKotlinPluginAccessors;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AndroidPluginAccessors extends PluginFactory {
|
|
||||||
|
|
||||||
public AndroidPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin provider for <b>android.application</b> with plugin id <b>com.android.application</b> and
|
|
||||||
* with version reference <b>agp</b>
|
|
||||||
* <p>
|
|
||||||
* This plugin was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<PluginDependency> getApplication() { return createPlugin("android.application"); }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class KotlinPluginAccessors extends PluginFactory {
|
|
||||||
|
|
||||||
public KotlinPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin provider for <b>kotlin.android</b> with plugin id <b>org.jetbrains.kotlin.android</b> and
|
|
||||||
* with version reference <b>kotlin</b>
|
|
||||||
* <p>
|
|
||||||
* This plugin was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<PluginDependency> getAndroid() { return createPlugin("kotlin.android"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin provider for <b>kotlin.compose</b> with plugin id <b>org.jetbrains.kotlin.plugin.compose</b> and
|
|
||||||
* with version reference <b>kotlin</b>
|
|
||||||
* <p>
|
|
||||||
* This plugin was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<PluginDependency> getCompose() { return createPlugin("kotlin.compose"); }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Plugin provider for <b>kotlin.serialization</b> with plugin id <b>org.jetbrains.kotlin.plugin.serialization</b> and
|
|
||||||
* with version reference <b>kotlin</b>
|
|
||||||
* <p>
|
|
||||||
* This plugin was declared in catalog libs.versions.toml
|
|
||||||
*/
|
|
||||||
public Provider<PluginDependency> getSerialization() { return createPlugin("kotlin.serialization"); }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
|||||||
#Fri Aug 15 14:37:16 MDT 2025
|
|
||||||
gradle.version=8.11.1
|
|
||||||
Binary file not shown.
@@ -1,2 +0,0 @@
|
|||||||
#Fri Aug 15 12:29:02 MDT 2025
|
|
||||||
java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home
|
|
||||||
Binary file not shown.
25
.idea/caches/deviceStreaming.xml
generated
25
.idea/caches/deviceStreaming.xml
generated
@@ -51,6 +51,18 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2412" />
|
<option name="screenY" value="2412" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="35" />
|
||||||
|
<option name="brand" value="OnePlus" />
|
||||||
|
<option name="codename" value="OP5552L1" />
|
||||||
|
<option name="id" value="OP5552L1" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<option name="manufacturer" value="OnePlus" />
|
||||||
|
<option name="name" value="CPH2415" />
|
||||||
|
<option name="screenDensity" value="480" />
|
||||||
|
<option name="screenX" value="1080" />
|
||||||
|
<option name="screenY" value="2412" />
|
||||||
|
</PersistentDeviceSelectionData>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="OPPO" />
|
<option name="brand" value="OPPO" />
|
||||||
@@ -817,6 +829,19 @@
|
|||||||
<option name="screenX" value="1080" />
|
<option name="screenX" value="1080" />
|
||||||
<option name="screenY" value="2424" />
|
<option name="screenY" value="2424" />
|
||||||
</PersistentDeviceSelectionData>
|
</PersistentDeviceSelectionData>
|
||||||
|
<PersistentDeviceSelectionData>
|
||||||
|
<option name="api" value="36" />
|
||||||
|
<option name="brand" value="google" />
|
||||||
|
<option name="codename" value="tokay" />
|
||||||
|
<option name="default" value="true" />
|
||||||
|
<option name="id" value="tokay" />
|
||||||
|
<option name="labId" value="google" />
|
||||||
|
<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>
|
||||||
<PersistentDeviceSelectionData>
|
<PersistentDeviceSelectionData>
|
||||||
<option name="api" value="34" />
|
<option name="api" value="34" />
|
||||||
<option name="brand" value="samsung" />
|
<option name="brand" value="samsung" />
|
||||||
|
|||||||
8
.idea/deploymentTargetSelector.xml
generated
8
.idea/deploymentTargetSelector.xml
generated
@@ -4,6 +4,14 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DropdownSelection timestamp="2025-09-07T04:49:14.182787Z">
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/atridad/.android/avd/Medium_Phone.avd" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</DropdownSelection>
|
||||||
|
<DialogSelection />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
@@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK" />
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="temurin-21" project-jdk-type="JavaSDK" />
|
||||||
</project>
|
</project>
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
kotlin version: 2.0.21
|
|
||||||
error message: java.lang.IllegalStateException: Storage for [/Users/atridad/Developer/personal/OpenClimb/app/build/kspCaches/debug/symbolLookups/id-to-file.tab] is already registered
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.<init>(PagedFileStorage.java:72)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.<init>(ResizeableMappedFile.java:55)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.<init>(PersistentBTreeEnumerator.java:128)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:165)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:140)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:45)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:71)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.set(PersistentStorage.kt:96)
|
|
||||||
at org.jetbrains.kotlin.incremental.LookupStorage.addFileIfNeeded(LookupStorage.kt:165)
|
|
||||||
at org.jetbrains.kotlin.incremental.LookupStorage.addAll$lambda$4(LookupStorage.kt:117)
|
|
||||||
at org.jetbrains.kotlin.utils.CollectionsKt.keysToMap(collections.kt:117)
|
|
||||||
at org.jetbrains.kotlin.incremental.LookupStorage.addAll(LookupStorage.kt:117)
|
|
||||||
at org.jetbrains.kotlin.incremental.BuildUtilKt.update(buildUtil.kt:134)
|
|
||||||
at com.google.devtools.ksp.LookupStorageWrapperImpl.update(IncrementalContext.kt:231)
|
|
||||||
at com.google.devtools.ksp.common.IncrementalContextBase.updateLookupCache(IncrementalContextBase.kt:133)
|
|
||||||
at com.google.devtools.ksp.common.IncrementalContextBase.updateCaches(IncrementalContextBase.kt:365)
|
|
||||||
at com.google.devtools.ksp.common.IncrementalContextBase.updateCachesAndOutputs(IncrementalContextBase.kt:471)
|
|
||||||
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:362)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
|
|
||||||
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.repeatAnalysisIfNeeded(KotlinToJVMBytecodeCompiler.kt:282)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runFrontendAndGenerateIrUsingClassicFrontend(KotlinToJVMBytecodeCompiler.kt:195)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:106)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:170)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
|
|
||||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
|
|
||||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
|
|
||||||
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
|
|
||||||
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1555)
|
|
||||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
|
||||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
|
||||||
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
|
|
||||||
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
|
|
||||||
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
|
|
||||||
at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
|
|
||||||
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
|
|
||||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)
|
|
||||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)
|
|
||||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)
|
|
||||||
at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
|
|
||||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)
|
|
||||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
|
||||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
|
||||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
|
||||||
Suppressed: java.lang.Exception: Storage[/Users/atridad/Developer/personal/OpenClimb/app/build/kspCaches/debug/symbolLookups/id-to-file.tab] registration stack trace
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.<init>(PagedFileStorage.java:72)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.<init>(ResizeableMappedFile.java:55)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.<init>(PersistentBTreeEnumerator.java:128)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:165)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:140)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:45)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:71)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageIfExists(LazyStorage.kt:53)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.get(LazyStorage.kt:76)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.get(PersistentStorage.kt:92)
|
|
||||||
at org.jetbrains.kotlin.incremental.LookupStorage.get(LookupStorage.kt:99)
|
|
||||||
at com.google.devtools.ksp.LookupStorageWrapperImpl.get(IncrementalContext.kt:224)
|
|
||||||
at com.google.devtools.ksp.common.IncrementalContextBase.calcDirtyFiles(IncrementalContextBase.kt:234)
|
|
||||||
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:196)
|
|
||||||
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:189)
|
|
||||||
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.handleException(KotlinSymbolProcessingExtension.kt:414)
|
|
||||||
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:189)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
|
|
||||||
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
|
|
||||||
... 23 more
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
kotlin version: 2.0.21
|
|
||||||
error message: java.lang.IllegalStateException: Storage for [/Users/atridad/Developer/personal/OpenClimb/app/build/kspCaches/debug/symbolLookups/id-to-file.tab] is already registered
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.<init>(PagedFileStorage.java:72)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.<init>(ResizeableMappedFile.java:55)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.<init>(PersistentBTreeEnumerator.java:128)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:165)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:140)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:45)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:71)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.set(PersistentStorage.kt:96)
|
|
||||||
at org.jetbrains.kotlin.incremental.LookupStorage.addFileIfNeeded(LookupStorage.kt:165)
|
|
||||||
at org.jetbrains.kotlin.incremental.LookupStorage.addAll$lambda$4(LookupStorage.kt:117)
|
|
||||||
at org.jetbrains.kotlin.utils.CollectionsKt.keysToMap(collections.kt:117)
|
|
||||||
at org.jetbrains.kotlin.incremental.LookupStorage.addAll(LookupStorage.kt:117)
|
|
||||||
at org.jetbrains.kotlin.incremental.BuildUtilKt.update(buildUtil.kt:134)
|
|
||||||
at com.google.devtools.ksp.LookupStorageWrapperImpl.update(IncrementalContext.kt:231)
|
|
||||||
at com.google.devtools.ksp.common.IncrementalContextBase.updateLookupCache(IncrementalContextBase.kt:133)
|
|
||||||
at com.google.devtools.ksp.common.IncrementalContextBase.updateCaches(IncrementalContextBase.kt:365)
|
|
||||||
at com.google.devtools.ksp.common.IncrementalContextBase.updateCachesAndOutputs(IncrementalContextBase.kt:471)
|
|
||||||
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:362)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
|
|
||||||
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.repeatAnalysisIfNeeded(KotlinToJVMBytecodeCompiler.kt:282)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runFrontendAndGenerateIrUsingClassicFrontend(KotlinToJVMBytecodeCompiler.kt:195)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:106)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:170)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
|
|
||||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
|
|
||||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
|
|
||||||
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
|
|
||||||
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1555)
|
|
||||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
|
|
||||||
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
|
|
||||||
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
|
|
||||||
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
|
|
||||||
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
|
|
||||||
at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
|
|
||||||
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
|
|
||||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)
|
|
||||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)
|
|
||||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)
|
|
||||||
at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
|
|
||||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)
|
|
||||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
|
|
||||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
|
|
||||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
|
||||||
Suppressed: java.lang.Exception: Storage[/Users/atridad/Developer/personal/OpenClimb/app/build/kspCaches/debug/symbolLookups/id-to-file.tab] registration stack trace
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.<init>(PagedFileStorage.java:72)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.<init>(ResizeableMappedFile.java:55)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.<init>(PersistentBTreeEnumerator.java:128)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:165)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:140)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:45)
|
|
||||||
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:71)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageIfExists(LazyStorage.kt:53)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.LazyStorage.get(LazyStorage.kt:76)
|
|
||||||
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.get(PersistentStorage.kt:92)
|
|
||||||
at org.jetbrains.kotlin.incremental.LookupStorage.get(LookupStorage.kt:99)
|
|
||||||
at com.google.devtools.ksp.LookupStorageWrapperImpl.get(IncrementalContext.kt:224)
|
|
||||||
at com.google.devtools.ksp.common.IncrementalContextBase.calcDirtyFiles(IncrementalContextBase.kt:234)
|
|
||||||
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:196)
|
|
||||||
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:189)
|
|
||||||
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.handleException(KotlinSymbolProcessingExtension.kt:414)
|
|
||||||
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:189)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
|
|
||||||
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
|
|
||||||
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
|
|
||||||
... 23 more
|
|
||||||
|
|
||||||
|
|
||||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"java.configuration.updateBuildConfiguration": "disabled"
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,8 @@ This is a FOSS Android app meant to help climbers track their sessions, routes/p
|
|||||||
|
|
||||||
You have two options:
|
You have two options:
|
||||||
|
|
||||||
1. Download the latest APK from the Released page
|
1. Download the latest APK from the Releases page
|
||||||
2. Use <a href="">Obtainium</a>
|
2. [<img src="https://github.com/ImranR98/Obtainium/blob/main/assets/graphics/badge_obtainium.png?raw=true" alt="Obtainium" height="41">](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22com.atridad.openclimb%22%2C%22url%22%3A%22https%3A%2F%2Fgit.atri.dad%2Fatridad%2FOpenClimb%2Freleases%22%2C%22author%22%3A%22git.atri.dad%22%2C%22name%22%3A%22OpenClimb%22%2C%22preferredApkIndex%22%3A0%2C%22additionalSettings%22%3A%22%7B%5C%22intermediateLink%5C%22%3A%5B%5D%2C%5C%22customLinkFilterRegex%5C%22%3A%5C%22%5C%22%2C%5C%22filterByLinkText%5C%22%3Afalse%2C%5C%22skipSort%5C%22%3Afalse%2C%5C%22reverseSort%5C%22%3Afalse%2C%5C%22sortByLastLinkSegment%5C%22%3Afalse%2C%5C%22versionExtractWholePage%5C%22%3Afalse%2C%5C%22requestHeader%5C%22%3A%5B%7B%5C%22requestHeader%5C%22%3A%5C%22User-Agent%3A%20Mozilla%2F5.0%20(Linux%3B%20Android%2010%3B%20K)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F114.0.0.0%20Mobile%20Safari%2F537.36%5C%22%7D%5D%2C%5C%22defaultPseudoVersioningMethod%5C%22%3A%5C%22partialAPKHash%5C%22%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionExtractionRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22matchGroupToUse%5C%22%3A%5C%22%5C%22%2C%5C%22versionDetection%5C%22%3Afalse%2C%5C%22useVersionCodeAsOSVersion%5C%22%3Afalse%2C%5C%22apkFilterRegEx%5C%22%3A%5C%22%5C%22%2C%5C%22invertAPKFilter%5C%22%3Afalse%2C%5C%22autoApkFilterByArch%5C%22%3Atrue%2C%5C%22appName%5C%22%3A%5C%22OpenClimb%5C%22%2C%5C%22appAuthor%5C%22%3A%5C%22%5C%22%2C%5C%22shizukuPretendToBeGooglePlay%5C%22%3Afalse%2C%5C%22allowInsecure%5C%22%3Afalse%2C%5C%22exemptFromBackgroundUpdates%5C%22%3Afalse%2C%5C%22skipUpdateNotifications%5C%22%3Afalse%2C%5C%22about%5C%22%3A%5C%22%5C%22%2C%5C%22refreshBeforeDownload%5C%22%3Afalse%7D%22%2C%22overrideSource%22%3Anull%7D)
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
@@ -8,14 +10,14 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.atridad.openclimb"
|
namespace = "com.atridad.openclimb"
|
||||||
compileSdk = 35
|
compileSdk = 36
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.atridad.openclimb"
|
applicationId = "com.atridad.openclimb"
|
||||||
minSdk = 31
|
minSdk = 34
|
||||||
targetSdk = 35
|
targetSdk = 36
|
||||||
versionCode = 5
|
versionCode = 22
|
||||||
versionName = "0.3.2"
|
versionName = "1.4.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -30,17 +32,27 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "11"
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion.set(JavaLanguageVersion.of(17))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
compilerOptions {
|
||||||
|
jvmTarget.set(JvmTarget.JVM_17)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Core Android libraries
|
// Core Android libraries
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
@@ -57,6 +69,7 @@ dependencies {
|
|||||||
// Room Database
|
// Room Database
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
implementation(libs.androidx.room.ktx)
|
implementation(libs.androidx.room.ktx)
|
||||||
|
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
@@ -74,13 +87,18 @@ dependencies {
|
|||||||
// Image Loading
|
// Image Loading
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
// Charts - Placeholder for future implementation
|
|
||||||
// Charts will be implemented with a stable library in future versions
|
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
|
testImplementation(libs.mockk)
|
||||||
|
testImplementation(libs.kotlinx.coroutines.test)
|
||||||
androidTestImplementation(libs.androidx.junit)
|
androidTestImplementation(libs.androidx.junit)
|
||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
|
androidTestImplementation(libs.androidx.test.core)
|
||||||
|
androidTestImplementation(libs.androidx.test.ext)
|
||||||
|
androidTestImplementation(libs.androidx.test.runner)
|
||||||
|
androidTestImplementation(libs.androidx.test.rules)
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||||
debugImplementation(libs.androidx.ui.tooling)
|
debugImplementation(libs.androidx.ui.tooling)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"artifactType": {
|
|
||||||
"type": "APK",
|
|
||||||
"kind": "Directory"
|
|
||||||
},
|
|
||||||
"applicationId": "com.atridad.openclimb",
|
|
||||||
"variantName": "release",
|
|
||||||
"elements": [
|
|
||||||
{
|
|
||||||
"type": "SINGLE",
|
|
||||||
"filters": [],
|
|
||||||
"attributes": [],
|
|
||||||
"versionCode": 4,
|
|
||||||
"versionName": "0.3.1",
|
|
||||||
"outputFile": "app-release.apk"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"elementType": "File",
|
|
||||||
"baselineProfiles": [
|
|
||||||
{
|
|
||||||
"minApi": 28,
|
|
||||||
"maxApi": 30,
|
|
||||||
"baselineProfiles": [
|
|
||||||
"baselineProfiles/1/app-release.dm"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"minApi": 31,
|
|
||||||
"maxApi": 2147483647,
|
|
||||||
"baselineProfiles": [
|
|
||||||
"baselineProfiles/0/app-release.dm"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"minSdkVersionForDexing": 31
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
android:maxSdkVersion="28" />
|
android:maxSdkVersion="28" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
|
<!-- Hardware features -->
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
|
|
||||||
<!-- Permissions for notifications and foreground service -->
|
<!-- Permissions for notifications and foreground service -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
@@ -27,12 +30,14 @@
|
|||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.OpenClimb">
|
android:theme="@style/Theme.OpenClimb.Splash">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- FileProvider for sharing images -->
|
<!-- FileProvider for sharing images -->
|
||||||
@@ -51,11 +56,24 @@
|
|||||||
android:name=".service.SessionTrackingService"
|
android:name=".service.SessionTrackingService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="specialUse">
|
android:foregroundServiceType="specialUse"
|
||||||
|
android:description="@string/session_tracking_service_description">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.foreground_service_type"
|
android:name="android.app.foreground_service_type"
|
||||||
android:value="specialUse" />
|
android:value="specialUse" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<!-- Widget Provider -->
|
||||||
|
<receiver
|
||||||
|
android:name=".widget.ClimbStatsWidgetProvider"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/widget_climb_stats_info" />
|
||||||
|
</receiver>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,27 +1,52 @@
|
|||||||
package com.atridad.openclimb
|
package com.atridad.openclimb
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.atridad.openclimb.ui.OpenClimbApp
|
import com.atridad.openclimb.ui.OpenClimbApp
|
||||||
import com.atridad.openclimb.ui.theme.OpenClimbTheme
|
import com.atridad.openclimb.ui.theme.OpenClimbTheme
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
private var shortcutAction by mutableStateOf<String?>(null)
|
||||||
|
private var lastUsedGymId by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
|
fun clearShortcutAction() {
|
||||||
|
shortcutAction = null
|
||||||
|
lastUsedGymId = null
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
setTheme(R.style.Theme_OpenClimb)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
shortcutAction = intent?.action
|
||||||
|
lastUsedGymId = intent?.getStringExtra("LAST_USED_GYM_ID")
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
OpenClimbTheme {
|
OpenClimbTheme {
|
||||||
Surface(
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
modifier = Modifier.fillMaxSize()
|
OpenClimbApp(
|
||||||
) {
|
shortcutAction = shortcutAction,
|
||||||
OpenClimbApp()
|
lastUsedGymId = lastUsedGymId,
|
||||||
|
onShortcutActionProcessed = { clearShortcutAction() }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
|
||||||
|
shortcutAction = intent.action
|
||||||
|
lastUsedGymId = intent.getStringExtra("LAST_USED_GYM_ID")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -53,6 +53,9 @@ interface AttemptDao {
|
|||||||
@Query("SELECT COUNT(*) FROM attempts")
|
@Query("SELECT COUNT(*) FROM attempts")
|
||||||
suspend fun getAttemptsCount(): Int
|
suspend fun getAttemptsCount(): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM attempts")
|
||||||
|
suspend fun deleteAllAttempts()
|
||||||
|
|
||||||
@Query("SELECT COUNT(*) FROM attempts WHERE sessionId = :sessionId")
|
@Query("SELECT COUNT(*) FROM attempts WHERE sessionId = :sessionId")
|
||||||
suspend fun getAttemptsCountBySession(sessionId: String): Int
|
suspend fun getAttemptsCountBySession(sessionId: String): Int
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ interface ClimbSessionDao {
|
|||||||
@Query("SELECT * FROM climb_sessions WHERE status = 'ACTIVE' ORDER BY date DESC LIMIT 1")
|
@Query("SELECT * FROM climb_sessions WHERE status = 'ACTIVE' ORDER BY date DESC LIMIT 1")
|
||||||
suspend fun getActiveSession(): ClimbSession?
|
suspend fun getActiveSession(): ClimbSession?
|
||||||
|
|
||||||
|
@Query("DELETE FROM climb_sessions")
|
||||||
|
suspend fun deleteAllSessions()
|
||||||
|
|
||||||
@Query("SELECT * FROM climb_sessions WHERE status = 'ACTIVE' ORDER BY date DESC LIMIT 1")
|
@Query("SELECT * FROM climb_sessions WHERE status = 'ACTIVE' ORDER BY date DESC LIMIT 1")
|
||||||
fun getActiveSessionFlow(): Flow<ClimbSession?>
|
fun getActiveSessionFlow(): Flow<ClimbSession?>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,4 +37,7 @@ interface GymDao {
|
|||||||
|
|
||||||
@Query("SELECT * FROM gyms WHERE name LIKE '%' || :searchQuery || '%' OR location LIKE '%' || :searchQuery || '%'")
|
@Query("SELECT * FROM gyms WHERE name LIKE '%' || :searchQuery || '%' OR location LIKE '%' || :searchQuery || '%'")
|
||||||
fun searchGyms(searchQuery: String): Flow<List<Gym>>
|
fun searchGyms(searchQuery: String): Flow<List<Gym>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM gyms")
|
||||||
|
suspend fun deleteAllGyms()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,4 +59,10 @@ interface ProblemDao {
|
|||||||
ORDER BY updatedAt DESC
|
ORDER BY updatedAt DESC
|
||||||
""")
|
""")
|
||||||
fun searchProblems(searchQuery: String): Flow<List<Problem>>
|
fun searchProblems(searchQuery: String): Flow<List<Problem>>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM problems")
|
||||||
|
suspend fun getProblemsCount(): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM problems")
|
||||||
|
suspend fun deleteAllProblems()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ import java.time.LocalDateTime
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class AttemptResult {
|
enum class AttemptResult {
|
||||||
SUCCESS, // Completed the problem/route
|
SUCCESS,
|
||||||
FALL, // Fell but made progress
|
FALL,
|
||||||
NO_PROGRESS, // Couldn't make meaningful progress
|
NO_PROGRESS,
|
||||||
FLASH, // Completed on first try
|
FLASH,
|
||||||
REDPOINT, // Completed after previous attempts
|
|
||||||
ONSIGHT // Completed on first try without prior knowledge
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ data class ClimbSession(
|
|||||||
val start = LocalDateTime.parse(startTime)
|
val start = LocalDateTime.parse(startTime)
|
||||||
val end = LocalDateTime.parse(endTime)
|
val end = LocalDateTime.parse(endTime)
|
||||||
java.time.Duration.between(start, end).toMinutes()
|
java.time.Duration.between(start, end).toMinutes()
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
} else null
|
} else null
|
||||||
|
|||||||
@@ -68,4 +68,41 @@ data class DifficultyGrade(
|
|||||||
val system: DifficultySystem,
|
val system: DifficultySystem,
|
||||||
val grade: String,
|
val grade: String,
|
||||||
val numericValue: Int
|
val numericValue: Int
|
||||||
)
|
) {
|
||||||
|
/**
|
||||||
|
* Compare this grade with another grade of the same system
|
||||||
|
* Returns negative if this grade is easier, positive if harder, 0 if equal
|
||||||
|
*/
|
||||||
|
fun compareTo(other: DifficultyGrade): Int {
|
||||||
|
if (system != other.system) return 0
|
||||||
|
|
||||||
|
return when (system) {
|
||||||
|
DifficultySystem.V_SCALE -> compareVScaleGrades(grade, other.grade)
|
||||||
|
DifficultySystem.FONT -> compareFontGrades(grade, other.grade)
|
||||||
|
DifficultySystem.YDS -> compareYDSGrades(grade, other.grade)
|
||||||
|
DifficultySystem.CUSTOM -> grade.compareTo(other.grade)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compareVScaleGrades(grade1: String, grade2: String): Int {
|
||||||
|
// Handle VB (easiest) specially
|
||||||
|
if (grade1 == "VB" && grade2 != "VB") return -1
|
||||||
|
if (grade2 == "VB" && grade1 != "VB") return 1
|
||||||
|
if (grade1 == "VB" && grade2 == "VB") return 0
|
||||||
|
|
||||||
|
// Extract numeric values for V grades
|
||||||
|
val num1 = grade1.removePrefix("V").toIntOrNull() ?: 0
|
||||||
|
val num2 = grade2.removePrefix("V").toIntOrNull() ?: 0
|
||||||
|
return num1.compareTo(num2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compareFontGrades(grade1: String, grade2: String): Int {
|
||||||
|
// Simple string comparison for Font grades
|
||||||
|
return grade1.compareTo(grade2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compareYDSGrades(grade1: String, grade2: String): Int {
|
||||||
|
// Simple string comparison for YDS grades
|
||||||
|
return grade1.compareTo(grade2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +1,21 @@
|
|||||||
package com.atridad.openclimb.data.repository
|
package com.atridad.openclimb.data.repository
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Environment
|
|
||||||
import com.atridad.openclimb.data.database.OpenClimbDatabase
|
import com.atridad.openclimb.data.database.OpenClimbDatabase
|
||||||
import com.atridad.openclimb.data.model.*
|
import com.atridad.openclimb.data.model.*
|
||||||
import com.atridad.openclimb.utils.ZipExportImportUtils
|
import com.atridad.openclimb.utils.ZipExportImportUtils
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
class ClimbRepository(
|
class ClimbRepository(database: OpenClimbDatabase, private val context: Context) {
|
||||||
database: OpenClimbDatabase,
|
|
||||||
private val context: Context
|
|
||||||
) {
|
|
||||||
private val gymDao = database.gymDao()
|
private val gymDao = database.gymDao()
|
||||||
private val problemDao = database.problemDao()
|
private val problemDao = database.problemDao()
|
||||||
private val sessionDao = database.climbSessionDao()
|
private val sessionDao = database.climbSessionDao()
|
||||||
private val attemptDao = database.attemptDao()
|
private val attemptDao = database.attemptDao()
|
||||||
|
|
||||||
|
|
||||||
private val json = Json {
|
private val json = Json {
|
||||||
prettyPrint = true
|
prettyPrint = true
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
@@ -47,198 +41,195 @@ class ClimbRepository(
|
|||||||
// Session operations
|
// Session operations
|
||||||
fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions()
|
fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions()
|
||||||
suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id)
|
suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id)
|
||||||
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> = sessionDao.getSessionsByGym(gymId)
|
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> =
|
||||||
|
sessionDao.getSessionsByGym(gymId)
|
||||||
suspend fun getActiveSession(): ClimbSession? = sessionDao.getActiveSession()
|
suspend fun getActiveSession(): ClimbSession? = sessionDao.getActiveSession()
|
||||||
fun getActiveSessionFlow(): Flow<ClimbSession?> = sessionDao.getActiveSessionFlow()
|
fun getActiveSessionFlow(): Flow<ClimbSession?> = sessionDao.getActiveSessionFlow()
|
||||||
suspend fun insertSession(session: ClimbSession) = sessionDao.insertSession(session)
|
suspend fun insertSession(session: ClimbSession) = sessionDao.insertSession(session)
|
||||||
suspend fun updateSession(session: ClimbSession) = sessionDao.updateSession(session)
|
suspend fun updateSession(session: ClimbSession) = sessionDao.updateSession(session)
|
||||||
suspend fun deleteSession(session: ClimbSession) = sessionDao.deleteSession(session)
|
suspend fun deleteSession(session: ClimbSession) = sessionDao.deleteSession(session)
|
||||||
|
suspend fun getLastUsedGym(): Gym? {
|
||||||
|
val recentSessions = sessionDao.getRecentSessions(1).first()
|
||||||
|
return if (recentSessions.isNotEmpty()) {
|
||||||
|
getGymById(recentSessions.first().gymId)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Attempt operations
|
// Attempt operations
|
||||||
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
|
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
|
||||||
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> = attemptDao.getAttemptsBySession(sessionId)
|
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> =
|
||||||
fun getAttemptsByProblem(problemId: String): Flow<List<Attempt>> = attemptDao.getAttemptsByProblem(problemId)
|
attemptDao.getAttemptsBySession(sessionId)
|
||||||
|
fun getAttemptsByProblem(problemId: String): Flow<List<Attempt>> =
|
||||||
|
attemptDao.getAttemptsByProblem(problemId)
|
||||||
suspend fun insertAttempt(attempt: Attempt) = attemptDao.insertAttempt(attempt)
|
suspend fun insertAttempt(attempt: Attempt) = attemptDao.insertAttempt(attempt)
|
||||||
suspend fun updateAttempt(attempt: Attempt) = attemptDao.updateAttempt(attempt)
|
suspend fun updateAttempt(attempt: Attempt) = attemptDao.updateAttempt(attempt)
|
||||||
suspend fun deleteAttempt(attempt: Attempt) = attemptDao.deleteAttempt(attempt)
|
suspend fun deleteAttempt(attempt: Attempt) = attemptDao.deleteAttempt(attempt)
|
||||||
|
|
||||||
|
// ZIP Export with images - Single format for reliability
|
||||||
|
|
||||||
// JSON Export
|
|
||||||
suspend fun exportAllDataToJson(directory: File? = null): File {
|
|
||||||
val exportDir = directory ?: File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "OpenClimb")
|
|
||||||
if (!exportDir.exists()) {
|
|
||||||
exportDir.mkdirs()
|
|
||||||
}
|
|
||||||
|
|
||||||
val timestamp = LocalDateTime.now().toString().replace(":", "-").replace(".", "-")
|
|
||||||
val exportFile = File(exportDir, "openclimb_export_$timestamp.json")
|
|
||||||
|
|
||||||
val allGyms = gymDao.getAllGyms().first()
|
|
||||||
val allProblems = problemDao.getAllProblems().first()
|
|
||||||
val allSessions = sessionDao.getAllSessions().first()
|
|
||||||
val allAttempts = attemptDao.getAllAttempts().first()
|
|
||||||
|
|
||||||
val exportData = ClimbDataExport(
|
|
||||||
exportedAt = LocalDateTime.now().toString(),
|
|
||||||
gyms = allGyms,
|
|
||||||
problems = allProblems,
|
|
||||||
sessions = allSessions,
|
|
||||||
attempts = allAttempts
|
|
||||||
)
|
|
||||||
|
|
||||||
val jsonString = json.encodeToString(exportData)
|
|
||||||
exportFile.writeText(jsonString)
|
|
||||||
|
|
||||||
return exportFile
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun exportAllDataToUri(context: Context, uri: android.net.Uri) {
|
|
||||||
val gyms = gymDao.getAllGyms().first()
|
|
||||||
val problems = problemDao.getAllProblems().first()
|
|
||||||
val sessions = sessionDao.getAllSessions().first()
|
|
||||||
val attempts = attemptDao.getAllAttempts().first()
|
|
||||||
|
|
||||||
val exportData = ClimbDataExport(
|
|
||||||
exportedAt = LocalDateTime.now().toString(),
|
|
||||||
gyms = gyms,
|
|
||||||
problems = problems,
|
|
||||||
sessions = sessions,
|
|
||||||
attempts = attempts
|
|
||||||
)
|
|
||||||
|
|
||||||
val jsonString = json.encodeToString(exportData)
|
|
||||||
|
|
||||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
|
||||||
outputStream.write(jsonString.toByteArray())
|
|
||||||
} ?: throw Exception("Could not open output stream")
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun importDataFromJson(file: File) {
|
|
||||||
try {
|
|
||||||
val jsonContent = file.readText()
|
|
||||||
val importData = json.decodeFromString<ClimbDataExport>(jsonContent)
|
|
||||||
|
|
||||||
// Import gyms
|
|
||||||
importData.gyms.forEach { gym ->
|
|
||||||
try {
|
|
||||||
gymDao.insertGym(gym)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// If insertion fails, update instead
|
|
||||||
gymDao.updateGym(gym)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import problems
|
|
||||||
importData.problems.forEach { problem ->
|
|
||||||
try {
|
|
||||||
problemDao.insertProblem(problem)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
problemDao.updateProblem(problem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import sessions
|
|
||||||
importData.sessions.forEach { session ->
|
|
||||||
try {
|
|
||||||
sessionDao.insertSession(session)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
sessionDao.updateSession(session)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Import attempts
|
|
||||||
importData.attempts.forEach { attempt ->
|
|
||||||
try {
|
|
||||||
attemptDao.insertAttempt(attempt)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
attemptDao.updateAttempt(attempt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw Exception("Failed to import data: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZIP Export with images
|
|
||||||
suspend fun exportAllDataToZip(directory: File? = null): File {
|
suspend fun exportAllDataToZip(directory: File? = null): File {
|
||||||
|
try {
|
||||||
|
// Collect all data with proper error handling
|
||||||
val allGyms = gymDao.getAllGyms().first()
|
val allGyms = gymDao.getAllGyms().first()
|
||||||
val allProblems = problemDao.getAllProblems().first()
|
val allProblems = problemDao.getAllProblems().first()
|
||||||
val allSessions = sessionDao.getAllSessions().first()
|
val allSessions = sessionDao.getAllSessions().first()
|
||||||
val allAttempts = attemptDao.getAllAttempts().first()
|
val allAttempts = attemptDao.getAllAttempts().first()
|
||||||
|
|
||||||
val exportData = ClimbDataExport(
|
// Validate data integrity before export
|
||||||
|
validateDataIntegrity(allGyms, allProblems, allSessions, allAttempts)
|
||||||
|
|
||||||
|
val exportData =
|
||||||
|
ClimbDataExport(
|
||||||
exportedAt = LocalDateTime.now().toString(),
|
exportedAt = LocalDateTime.now().toString(),
|
||||||
|
version = "1.0",
|
||||||
gyms = allGyms,
|
gyms = allGyms,
|
||||||
problems = allProblems,
|
problems = allProblems,
|
||||||
sessions = allSessions,
|
sessions = allSessions,
|
||||||
attempts = allAttempts
|
attempts = allAttempts
|
||||||
)
|
)
|
||||||
|
|
||||||
// Collect all referenced image paths
|
// Collect all referenced image paths and validate they exist
|
||||||
val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet()
|
val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet()
|
||||||
|
val validImagePaths =
|
||||||
|
referencedImagePaths
|
||||||
|
.filter { imagePath ->
|
||||||
|
try {
|
||||||
|
val imageFile =
|
||||||
|
com.atridad.openclimb.utils.ImageUtils.getImageFile(
|
||||||
|
context,
|
||||||
|
imagePath
|
||||||
|
)
|
||||||
|
imageFile.exists() && imageFile.length() > 0
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
|
||||||
|
// Log any missing images for debugging
|
||||||
|
val missingImages = referencedImagePaths - validImagePaths
|
||||||
|
if (missingImages.isNotEmpty()) {
|
||||||
|
android.util.Log.w(
|
||||||
|
"ClimbRepository",
|
||||||
|
"Some referenced images are missing: $missingImages"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return ZipExportImportUtils.createExportZip(
|
return ZipExportImportUtils.createExportZip(
|
||||||
context = context,
|
context = context,
|
||||||
exportData = exportData,
|
exportData = exportData,
|
||||||
referencedImagePaths = referencedImagePaths,
|
referencedImagePaths = validImagePaths,
|
||||||
directory = directory
|
directory = directory
|
||||||
)
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Export failed: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun exportAllDataToZipUri(context: Context, uri: android.net.Uri) {
|
suspend fun exportAllDataToZipUri(context: Context, uri: android.net.Uri) {
|
||||||
val gyms = gymDao.getAllGyms().first()
|
try {
|
||||||
val problems = problemDao.getAllProblems().first()
|
// Collect all data with proper error handling
|
||||||
val sessions = sessionDao.getAllSessions().first()
|
val allGyms = gymDao.getAllGyms().first()
|
||||||
val attempts = attemptDao.getAllAttempts().first()
|
val allProblems = problemDao.getAllProblems().first()
|
||||||
|
val allSessions = sessionDao.getAllSessions().first()
|
||||||
|
val allAttempts = attemptDao.getAllAttempts().first()
|
||||||
|
|
||||||
val exportData = ClimbDataExport(
|
// Validate data integrity before export
|
||||||
|
validateDataIntegrity(allGyms, allProblems, allSessions, allAttempts)
|
||||||
|
|
||||||
|
val exportData =
|
||||||
|
ClimbDataExport(
|
||||||
exportedAt = LocalDateTime.now().toString(),
|
exportedAt = LocalDateTime.now().toString(),
|
||||||
gyms = gyms,
|
version = "1.0",
|
||||||
problems = problems,
|
gyms = allGyms,
|
||||||
sessions = sessions,
|
problems = allProblems,
|
||||||
attempts = attempts
|
sessions = allSessions,
|
||||||
|
attempts = allAttempts
|
||||||
)
|
)
|
||||||
|
|
||||||
// Collect all image paths
|
// Collect all referenced image paths and validate they exist
|
||||||
val referencedImagePaths = problems.flatMap { it.imagePaths }.toSet()
|
val referencedImagePaths = allProblems.flatMap { it.imagePaths }.toSet()
|
||||||
|
val validImagePaths =
|
||||||
|
referencedImagePaths
|
||||||
|
.filter { imagePath ->
|
||||||
|
try {
|
||||||
|
val imageFile =
|
||||||
|
com.atridad.openclimb.utils.ImageUtils.getImageFile(
|
||||||
|
context,
|
||||||
|
imagePath
|
||||||
|
)
|
||||||
|
imageFile.exists() && imageFile.length() > 0
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toSet()
|
||||||
|
|
||||||
ZipExportImportUtils.createExportZipToUri(
|
ZipExportImportUtils.createExportZipToUri(
|
||||||
context = context,
|
context = context,
|
||||||
uri = uri,
|
uri = uri,
|
||||||
exportData = exportData,
|
exportData = exportData,
|
||||||
referencedImagePaths = referencedImagePaths
|
referencedImagePaths = validImagePaths
|
||||||
)
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Export failed: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun importDataFromZip(file: File) {
|
suspend fun importDataFromZip(file: File) {
|
||||||
try {
|
try {
|
||||||
|
// Validate the ZIP file
|
||||||
|
if (!file.exists() || file.length() == 0L) {
|
||||||
|
throw Exception("Invalid ZIP file: file is empty or doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract and validate the ZIP contents
|
||||||
val importResult = ZipExportImportUtils.extractImportZip(context, file)
|
val importResult = ZipExportImportUtils.extractImportZip(context, file)
|
||||||
val importData = json.decodeFromString<ClimbDataExport>(importResult.jsonContent)
|
|
||||||
|
|
||||||
// Update problem image paths with the new imported paths
|
// Validate JSON content
|
||||||
val updatedProblems = ZipExportImportUtils.updateProblemImagePaths(
|
if (importResult.jsonContent.isBlank()) {
|
||||||
importData.problems,
|
throw Exception("Invalid ZIP file: no data.json found or empty content")
|
||||||
importResult.importedImagePaths
|
}
|
||||||
)
|
|
||||||
|
|
||||||
// Import gyms
|
// Parse and validate the data structure
|
||||||
|
val importData =
|
||||||
|
try {
|
||||||
|
json.decodeFromString<ClimbDataExport>(importResult.jsonContent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Invalid data format: ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate data integrity
|
||||||
|
validateImportData(importData)
|
||||||
|
|
||||||
|
// Clear existing data to avoid conflicts
|
||||||
|
attemptDao.deleteAllAttempts()
|
||||||
|
sessionDao.deleteAllSessions()
|
||||||
|
problemDao.deleteAllProblems()
|
||||||
|
gymDao.deleteAllGyms()
|
||||||
|
|
||||||
|
// Import gyms first (problems depend on gyms)
|
||||||
importData.gyms.forEach { gym ->
|
importData.gyms.forEach { gym ->
|
||||||
try {
|
try {
|
||||||
gymDao.insertGym(gym)
|
gymDao.insertGym(gym)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// If insertion fails update instead
|
throw Exception("Failed to import gym ${gym.name}: ${e.message}")
|
||||||
gymDao.updateGym(gym)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import problems with updated image paths
|
// Import problems with updated image paths
|
||||||
|
val updatedProblems =
|
||||||
|
ZipExportImportUtils.updateProblemImagePaths(
|
||||||
|
importData.problems,
|
||||||
|
importResult.importedImagePaths
|
||||||
|
)
|
||||||
|
|
||||||
updatedProblems.forEach { problem ->
|
updatedProblems.forEach { problem ->
|
||||||
try {
|
try {
|
||||||
problemDao.insertProblem(problem)
|
problemDao.insertProblem(problem)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
problemDao.updateProblem(problem)
|
throw Exception("Failed to import problem ${problem.name}: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,21 +238,104 @@ class ClimbRepository(
|
|||||||
try {
|
try {
|
||||||
sessionDao.insertSession(session)
|
sessionDao.insertSession(session)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
sessionDao.updateSession(session)
|
throw Exception("Failed to import session: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import attempts
|
// Import attempts last (depends on problems and sessions)
|
||||||
importData.attempts.forEach { attempt ->
|
importData.attempts.forEach { attempt ->
|
||||||
try {
|
try {
|
||||||
attemptDao.insertAttempt(attempt)
|
attemptDao.insertAttempt(attempt)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
attemptDao.updateAttempt(attempt)
|
throw Exception("Failed to import attempt: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw Exception("Import failed: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun validateDataIntegrity(
|
||||||
|
gyms: List<Gym>,
|
||||||
|
problems: List<Problem>,
|
||||||
|
sessions: List<ClimbSession>,
|
||||||
|
attempts: List<Attempt>
|
||||||
|
) {
|
||||||
|
// Validate that all problems reference valid gyms
|
||||||
|
val gymIds = gyms.map { it.id }.toSet()
|
||||||
|
val invalidProblems = problems.filter { it.gymId !in gymIds }
|
||||||
|
if (invalidProblems.isNotEmpty()) {
|
||||||
|
throw Exception(
|
||||||
|
"Data integrity error: ${invalidProblems.size} problems reference non-existent gyms"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that all sessions reference valid gyms
|
||||||
|
val invalidSessions = sessions.filter { it.gymId !in gymIds }
|
||||||
|
if (invalidSessions.isNotEmpty()) {
|
||||||
|
throw Exception(
|
||||||
|
"Data integrity error: ${invalidSessions.size} sessions reference non-existent gyms"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that all attempts reference valid problems and sessions
|
||||||
|
val problemIds = problems.map { it.id }.toSet()
|
||||||
|
val sessionIds = sessions.map { it.id }.toSet()
|
||||||
|
|
||||||
|
val invalidAttempts =
|
||||||
|
attempts.filter { it.problemId !in problemIds || it.sessionId !in sessionIds }
|
||||||
|
if (invalidAttempts.isNotEmpty()) {
|
||||||
|
throw Exception(
|
||||||
|
"Data integrity error: ${invalidAttempts.size} attempts reference non-existent problems or sessions"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateImportData(importData: ClimbDataExport) {
|
||||||
|
if (importData.gyms.isEmpty()) {
|
||||||
|
throw Exception("Import data is invalid: no gyms found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importData.version.isBlank()) {
|
||||||
|
throw Exception("Import data is invalid: no version information")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for reasonable data sizes to prevent malicious imports
|
||||||
|
if (importData.gyms.size > 1000 ||
|
||||||
|
importData.problems.size > 10000 ||
|
||||||
|
importData.sessions.size > 10000 ||
|
||||||
|
importData.attempts.size > 100000
|
||||||
|
) {
|
||||||
|
throw Exception("Import data is too large: possible corruption or malicious file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun resetAllData() {
|
||||||
|
try {
|
||||||
|
// Clear all data from database
|
||||||
|
attemptDao.deleteAllAttempts()
|
||||||
|
sessionDao.deleteAllSessions()
|
||||||
|
problemDao.deleteAllProblems()
|
||||||
|
gymDao.deleteAllGyms()
|
||||||
|
|
||||||
|
// Clear all images from storage
|
||||||
|
clearAllImages()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw Exception("Failed to import data: ${e.message}")
|
throw Exception("Reset failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearAllImages() {
|
||||||
|
try {
|
||||||
|
// Get the images directory
|
||||||
|
val imagesDir = File(context.filesDir, "images")
|
||||||
|
if (imagesDir.exists() && imagesDir.isDirectory) {
|
||||||
|
val deletedCount = imagesDir.listFiles()?.size ?: 0
|
||||||
|
imagesDir.deleteRecursively()
|
||||||
|
android.util.Log.i("ClimbRepository", "Cleared $deletedCount image files")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.w("ClimbRepository", "Failed to clear some images: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,6 +343,7 @@ class ClimbRepository(
|
|||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class ClimbDataExport(
|
data class ClimbDataExport(
|
||||||
val exportedAt: String,
|
val exportedAt: String,
|
||||||
|
val version: String = "1.0",
|
||||||
val gyms: List<Gym>,
|
val gyms: List<Gym>,
|
||||||
val problems: List<Problem>,
|
val problems: List<Problem>,
|
||||||
val sessions: List<ClimbSession>,
|
val sessions: List<ClimbSession>,
|
||||||
|
|||||||
@@ -4,13 +4,10 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
data class BottomNavigationItem(
|
data class BottomNavigationItem(val screen: Screen, val icon: ImageVector, val label: String)
|
||||||
val screen: Screen,
|
|
||||||
val icon: ImageVector,
|
|
||||||
val label: String
|
|
||||||
)
|
|
||||||
|
|
||||||
val bottomNavigationItems = listOf(
|
val bottomNavigationItems =
|
||||||
|
listOf(
|
||||||
BottomNavigationItem(
|
BottomNavigationItem(
|
||||||
screen = Screen.Sessions,
|
screen = Screen.Sessions,
|
||||||
icon = Icons.Default.PlayArrow,
|
icon = Icons.Default.PlayArrow,
|
||||||
|
|||||||
@@ -16,13 +16,16 @@ import kotlinx.coroutines.*
|
|||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
class SessionTrackingService : Service() {
|
class SessionTrackingService : Service() {
|
||||||
|
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
private var notificationJob: Job? = null
|
private var notificationJob: Job? = null
|
||||||
|
private var monitoringJob: Job? = null
|
||||||
|
|
||||||
private lateinit var repository: ClimbRepository
|
private lateinit var repository: ClimbRepository
|
||||||
|
private lateinit var notificationManager: NotificationManager
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val NOTIFICATION_ID = 1001
|
const val NOTIFICATION_ID = 1001
|
||||||
@@ -38,9 +41,10 @@ class SessionTrackingService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createStopIntent(context: Context): Intent {
|
fun createStopIntent(context: Context, sessionId: String): Intent {
|
||||||
return Intent(context, SessionTrackingService::class.java).apply {
|
return Intent(context, SessionTrackingService::class.java).apply {
|
||||||
action = ACTION_STOP_SESSION
|
action = ACTION_STOP_SESSION
|
||||||
|
putExtra(EXTRA_SESSION_ID, sessionId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,6 +54,7 @@ class SessionTrackingService : Service() {
|
|||||||
|
|
||||||
val database = OpenClimbDatabase.getDatabase(this)
|
val database = OpenClimbDatabase.getDatabase(this)
|
||||||
repository = ClimbRepository(database, this)
|
repository = ClimbRepository(database, this)
|
||||||
|
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
}
|
}
|
||||||
@@ -63,53 +68,143 @@ class SessionTrackingService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ACTION_STOP_SESSION -> {
|
ACTION_STOP_SESSION -> {
|
||||||
|
val sessionId = intent.getStringExtra(EXTRA_SESSION_ID)
|
||||||
|
serviceScope.launch {
|
||||||
|
try {
|
||||||
|
val targetSession = when {
|
||||||
|
sessionId != null -> repository.getSessionById(sessionId)
|
||||||
|
else -> repository.getActiveSession()
|
||||||
|
}
|
||||||
|
if (targetSession != null && targetSession.status == com.atridad.openclimb.data.model.SessionStatus.ACTIVE) {
|
||||||
|
val completed = with(com.atridad.openclimb.data.model.ClimbSession) { targetSession.complete() }
|
||||||
|
repository.updateSession(completed)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
stopSessionTracking()
|
stopSessionTracking()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return START_STICKY
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_REDELIVER_INTENT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
|
super.onTaskRemoved(rootIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
private fun startSessionTracking(sessionId: String) {
|
private fun startSessionTracking(sessionId: String) {
|
||||||
notificationJob?.cancel()
|
notificationJob?.cancel()
|
||||||
|
monitoringJob?.cancel()
|
||||||
|
|
||||||
|
try {
|
||||||
|
createAndShowNotification(sessionId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
notificationJob = serviceScope.launch {
|
notificationJob = serviceScope.launch {
|
||||||
|
try {
|
||||||
|
if (!isNotificationActive()) {
|
||||||
|
delay(1000L)
|
||||||
|
createAndShowNotification(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
|
delay(5000L)
|
||||||
updateNotification(sessionId)
|
updateNotification(sessionId)
|
||||||
delay(1000)
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
monitoringJob = serviceScope.launch {
|
||||||
|
try {
|
||||||
|
while (isActive) {
|
||||||
|
delay(10000L)
|
||||||
|
|
||||||
|
if (!isNotificationActive()) {
|
||||||
|
updateNotification(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val session = repository.getSessionById(sessionId)
|
||||||
|
if (session == null || session.status != com.atridad.openclimb.data.model.SessionStatus.ACTIVE) {
|
||||||
|
stopSessionTracking()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopSessionTracking() {
|
private fun stopSessionTracking() {
|
||||||
notificationJob?.cancel()
|
notificationJob?.cancel()
|
||||||
|
monitoringJob?.cancel()
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isNotificationActive(): Boolean {
|
||||||
|
return try {
|
||||||
|
val activeNotifications = notificationManager.activeNotifications
|
||||||
|
activeNotifications.any { it.id == NOTIFICATION_ID }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun updateNotification(sessionId: String) {
|
private suspend fun updateNotification(sessionId: String) {
|
||||||
try {
|
try {
|
||||||
val session = repository.getSessionById(sessionId)
|
createAndShowNotification(sessionId)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
|
||||||
|
try {
|
||||||
|
delay(10000L)
|
||||||
|
createAndShowNotification(sessionId)
|
||||||
|
} catch (retryException: Exception) {
|
||||||
|
retryException.printStackTrace()
|
||||||
|
stopSessionTracking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAndShowNotification(sessionId: String) {
|
||||||
|
try {
|
||||||
|
val session = runBlocking {
|
||||||
|
repository.getSessionById(sessionId)
|
||||||
|
}
|
||||||
if (session == null || session.status != com.atridad.openclimb.data.model.SessionStatus.ACTIVE) {
|
if (session == null || session.status != com.atridad.openclimb.data.model.SessionStatus.ACTIVE) {
|
||||||
stopSessionTracking()
|
stopSessionTracking()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val gym = repository.getGymById(session.gymId)
|
val gym = runBlocking {
|
||||||
val attempts = repository.getAttemptsBySession(sessionId).firstOrNull() ?: emptyList()
|
repository.getGymById(session.gymId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val attempts = runBlocking {
|
||||||
|
repository.getAttemptsBySession(sessionId).firstOrNull() ?: emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
val duration = session.startTime?.let { startTime ->
|
val duration = session.startTime?.let { startTime ->
|
||||||
try {
|
try {
|
||||||
val start = LocalDateTime.parse(startTime)
|
val start = LocalDateTime.parse(startTime)
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
val minutes = ChronoUnit.MINUTES.between(start, now)
|
val totalSeconds = ChronoUnit.SECONDS.between(start, now)
|
||||||
val hours = minutes / 60
|
val hours = totalSeconds / 3600
|
||||||
val remainingMinutes = minutes % 60
|
val minutes = (totalSeconds % 3600) / 60
|
||||||
|
val seconds = totalSeconds % 60
|
||||||
|
|
||||||
when {
|
when {
|
||||||
hours > 0 -> "${hours}h ${remainingMinutes}m"
|
hours > 0 -> "${hours}h ${minutes}m ${seconds}s"
|
||||||
remainingMinutes > 0 -> "${remainingMinutes}m"
|
minutes > 0 -> "${minutes}m ${seconds}s"
|
||||||
else -> "< 1m"
|
else -> "${totalSeconds}s"
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
"Active"
|
"Active"
|
||||||
@@ -121,7 +216,10 @@ class SessionTrackingService : Service() {
|
|||||||
.setContentText("${gym?.name ?: "Gym"} • $duration • ${attempts.size} attempts")
|
.setContentText("${gym?.name ?: "Gym"} • $duration • ${attempts.size} attempts")
|
||||||
.setSmallIcon(R.drawable.ic_mountains)
|
.setSmallIcon(R.drawable.ic_mountains)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setAutoCancel(false)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
.setContentIntent(createOpenAppIntent())
|
.setContentIntent(createOpenAppIntent())
|
||||||
.addAction(
|
.addAction(
|
||||||
R.drawable.ic_mountains,
|
R.drawable.ic_mountains,
|
||||||
@@ -131,20 +229,24 @@ class SessionTrackingService : Service() {
|
|||||||
.addAction(
|
.addAction(
|
||||||
android.R.drawable.ic_menu_close_clear_cancel,
|
android.R.drawable.ic_menu_close_clear_cancel,
|
||||||
"End Session",
|
"End Session",
|
||||||
createStopIntent()
|
createStopPendingIntent(sessionId)
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
} catch (_: Exception) {
|
|
||||||
// Handle errors gracefully
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
stopSessionTracking()
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createOpenAppIntent(): PendingIntent {
|
private fun createOpenAppIntent(): PendingIntent {
|
||||||
val intent = Intent(this, MainActivity::class.java).apply {
|
val intent = Intent(this, MainActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
action = "OPEN_SESSION"
|
||||||
}
|
}
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
@@ -154,8 +256,8 @@ class SessionTrackingService : Service() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createStopIntent(): PendingIntent {
|
private fun createStopPendingIntent(sessionId: String): PendingIntent {
|
||||||
val intent = createStopIntent(this)
|
val intent = createStopIntent(this, sessionId)
|
||||||
return PendingIntent.getService(
|
return PendingIntent.getService(
|
||||||
this,
|
this,
|
||||||
1,
|
1,
|
||||||
@@ -168,19 +270,23 @@ class SessionTrackingService : Service() {
|
|||||||
val channel = NotificationChannel(
|
val channel = NotificationChannel(
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
"Session Tracking",
|
"Session Tracking",
|
||||||
NotificationManager.IMPORTANCE_LOW
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
).apply {
|
).apply {
|
||||||
description = "Shows active climbing session information"
|
description = "Shows active climbing session information"
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
|
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
||||||
|
enableLights(false)
|
||||||
|
enableVibration(false)
|
||||||
|
setSound(null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
notificationJob?.cancel()
|
notificationJob?.cancel()
|
||||||
|
monitoringJob?.cancel()
|
||||||
serviceScope.cancel()
|
serviceScope.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
package com.atridad.openclimb.ui
|
package com.atridad.openclimb.ui
|
||||||
|
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
@@ -18,30 +22,155 @@ import com.atridad.openclimb.data.database.OpenClimbDatabase
|
|||||||
import com.atridad.openclimb.data.repository.ClimbRepository
|
import com.atridad.openclimb.data.repository.ClimbRepository
|
||||||
import com.atridad.openclimb.navigation.Screen
|
import com.atridad.openclimb.navigation.Screen
|
||||||
import com.atridad.openclimb.navigation.bottomNavigationItems
|
import com.atridad.openclimb.navigation.bottomNavigationItems
|
||||||
|
import com.atridad.openclimb.ui.components.NotificationPermissionDialog
|
||||||
import com.atridad.openclimb.ui.screens.*
|
import com.atridad.openclimb.ui.screens.*
|
||||||
import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
|
import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
|
||||||
import com.atridad.openclimb.ui.viewmodel.ClimbViewModelFactory
|
import com.atridad.openclimb.ui.viewmodel.ClimbViewModelFactory
|
||||||
|
import com.atridad.openclimb.utils.AppShortcutManager
|
||||||
|
import com.atridad.openclimb.utils.NotificationPermissionUtils
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun OpenClimbApp() {
|
fun OpenClimbApp(
|
||||||
|
shortcutAction: String? = null,
|
||||||
|
lastUsedGymId: String? = null,
|
||||||
|
onShortcutActionProcessed: () -> Unit = {}
|
||||||
|
) {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val currentBackStackEntry by navController.currentBackStackEntryAsState()
|
|
||||||
|
var lastUsedGym by remember { mutableStateOf<com.atridad.openclimb.data.model.Gym?>(null) }
|
||||||
|
|
||||||
val database = remember { OpenClimbDatabase.getDatabase(context) }
|
val database = remember { OpenClimbDatabase.getDatabase(context) }
|
||||||
val repository = remember { ClimbRepository(database, context) }
|
val repository = remember { ClimbRepository(database, context) }
|
||||||
val viewModel: ClimbViewModel = viewModel(
|
val viewModel: ClimbViewModel = viewModel(factory = ClimbViewModelFactory(repository))
|
||||||
factory = ClimbViewModelFactory(repository)
|
|
||||||
|
// Notification permission state
|
||||||
|
var showNotificationPermissionDialog by remember { mutableStateOf(false) }
|
||||||
|
var hasCheckedNotificationPermission by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Permission launcher
|
||||||
|
val permissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted: Boolean ->
|
||||||
|
if (!isGranted) {
|
||||||
|
showNotificationPermissionDialog = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (!hasCheckedNotificationPermission) {
|
||||||
|
hasCheckedNotificationPermission = true
|
||||||
|
|
||||||
|
if (NotificationPermissionUtils.shouldRequestNotificationPermission() &&
|
||||||
|
!NotificationPermissionUtils.isNotificationPermissionGranted(context)
|
||||||
|
) {
|
||||||
|
showNotificationPermissionDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) { viewModel.ensureSessionTrackingServiceRunning(context) }
|
||||||
|
|
||||||
|
val activeSession by viewModel.activeSession.collectAsState()
|
||||||
|
val gyms by viewModel.gyms.collectAsState()
|
||||||
|
|
||||||
|
// Update last used gym when gyms change
|
||||||
|
LaunchedEffect(gyms) {
|
||||||
|
if (gyms.isNotEmpty() && lastUsedGym == null) {
|
||||||
|
lastUsedGym = viewModel.getLastUsedGym()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(activeSession, gyms, lastUsedGym) {
|
||||||
|
AppShortcutManager.updateShortcuts(
|
||||||
|
context = context,
|
||||||
|
hasActiveSession = activeSession != null,
|
||||||
|
hasGyms = gyms.isNotEmpty(),
|
||||||
|
lastUsedGym = if (activeSession == null && gyms.size > 1) lastUsedGym else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(shortcutAction) {
|
||||||
|
when (shortcutAction) {
|
||||||
|
AppShortcutManager.ACTION_START_SESSION -> {
|
||||||
|
navController.navigate(Screen.Sessions) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppShortcutManager.ACTION_END_SESSION -> {
|
||||||
|
navController.navigate(Screen.Sessions) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
|
||||||
|
activeSession?.let { session -> viewModel.endSession(context, session.id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process shortcut actions after data is loaded
|
||||||
|
LaunchedEffect(shortcutAction, activeSession, gyms, lastUsedGym) {
|
||||||
|
if (shortcutAction == AppShortcutManager.ACTION_START_SESSION && gyms.isNotEmpty()) {
|
||||||
|
android.util.Log.d(
|
||||||
|
"OpenClimbApp",
|
||||||
|
"Processing shortcut action: activeSession=$activeSession, gyms.size=${gyms.size}, lastUsedGymId=$lastUsedGymId, lastUsedGym=${lastUsedGym?.name}"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FAB configuration
|
if (activeSession == null) {
|
||||||
|
if (NotificationPermissionUtils.shouldRequestNotificationPermission() &&
|
||||||
|
!NotificationPermissionUtils.isNotificationPermissionGranted(
|
||||||
|
context
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
android.util.Log.d("OpenClimbApp", "Showing notification permission dialog")
|
||||||
|
showNotificationPermissionDialog = true
|
||||||
|
} else {
|
||||||
|
if (gyms.size == 1) {
|
||||||
|
android.util.Log.d(
|
||||||
|
"OpenClimbApp",
|
||||||
|
"Starting session with single gym: ${gyms.first().name}"
|
||||||
|
)
|
||||||
|
viewModel.startSession(context, gyms.first().id)
|
||||||
|
} else {
|
||||||
|
// Try to get the last used gym from the intent or fallback to state
|
||||||
|
val targetGym =
|
||||||
|
lastUsedGymId?.let { gymId -> gyms.find { it.id == gymId } }
|
||||||
|
?: lastUsedGym
|
||||||
|
|
||||||
|
if (targetGym != null) {
|
||||||
|
android.util.Log.d(
|
||||||
|
"OpenClimbApp",
|
||||||
|
"Starting session with target gym: ${targetGym.name}"
|
||||||
|
)
|
||||||
|
viewModel.startSession(context, targetGym.id)
|
||||||
|
} else {
|
||||||
|
android.util.Log.d(
|
||||||
|
"OpenClimbApp",
|
||||||
|
"No target gym found, navigating to selection"
|
||||||
|
)
|
||||||
|
navController.navigate(Screen.AddEditSession())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
android.util.Log.d(
|
||||||
|
"OpenClimbApp",
|
||||||
|
"Active session already exists: ${activeSession?.id}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the shortcut action after processing to prevent repeated execution
|
||||||
|
onShortcutActionProcessed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var fabConfig by remember { mutableStateOf<FabConfig?>(null) }
|
var fabConfig by remember { mutableStateOf<FabConfig?>(null) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = { OpenClimbBottomNavigation(navController = navController) },
|
||||||
OpenClimbBottomNavigation(navController = navController)
|
|
||||||
},
|
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
fabConfig?.let { config ->
|
fabConfig?.let { config ->
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
@@ -61,22 +190,32 @@ fun OpenClimbApp() {
|
|||||||
startDestination = Screen.Sessions,
|
startDestination = Screen.Sessions,
|
||||||
modifier = Modifier.padding(innerPadding)
|
modifier = Modifier.padding(innerPadding)
|
||||||
) {
|
) {
|
||||||
// Main screens
|
|
||||||
composable<Screen.Sessions> {
|
composable<Screen.Sessions> {
|
||||||
val gyms by viewModel.gyms.collectAsState()
|
|
||||||
val activeSession by viewModel.activeSession.collectAsState()
|
|
||||||
LaunchedEffect(gyms, activeSession) {
|
LaunchedEffect(gyms, activeSession) {
|
||||||
fabConfig = if (gyms.isNotEmpty() && activeSession == null) {
|
fabConfig =
|
||||||
|
if (gyms.isNotEmpty() && activeSession == null) {
|
||||||
FabConfig(
|
FabConfig(
|
||||||
icon = Icons.Default.Add,
|
icon = Icons.Default.PlayArrow,
|
||||||
contentDescription = "Start Session",
|
contentDescription = "Start Session",
|
||||||
onClick = {
|
onClick = {
|
||||||
|
if (NotificationPermissionUtils
|
||||||
|
.shouldRequestNotificationPermission() &&
|
||||||
|
!NotificationPermissionUtils
|
||||||
|
.isNotificationPermissionGranted(
|
||||||
|
context
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
showNotificationPermissionDialog = true
|
||||||
|
} else {
|
||||||
if (gyms.size == 1) {
|
if (gyms.size == 1) {
|
||||||
viewModel.startSession(context, gyms.first().id)
|
viewModel.startSession(context, gyms.first().id)
|
||||||
} else {
|
} else {
|
||||||
|
// Always show gym selection for FAB when
|
||||||
|
// multiple gyms
|
||||||
navController.navigate(Screen.AddEditSession())
|
navController.navigate(Screen.AddEditSession())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
@@ -91,9 +230,9 @@ fun OpenClimbApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composable<Screen.Problems> {
|
composable<Screen.Problems> {
|
||||||
val gyms by viewModel.gyms.collectAsState()
|
|
||||||
LaunchedEffect(gyms) {
|
LaunchedEffect(gyms) {
|
||||||
fabConfig = if (gyms.isNotEmpty()) {
|
fabConfig =
|
||||||
|
if (gyms.isNotEmpty()) {
|
||||||
FabConfig(
|
FabConfig(
|
||||||
icon = Icons.Default.Add,
|
icon = Icons.Default.Add,
|
||||||
contentDescription = "Add Problem",
|
contentDescription = "Add Problem",
|
||||||
@@ -114,20 +253,17 @@ fun OpenClimbApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composable<Screen.Analytics> {
|
composable<Screen.Analytics> {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) { fabConfig = null }
|
||||||
fabConfig = null // No FAB for analytics
|
|
||||||
}
|
|
||||||
AnalyticsScreen(viewModel = viewModel)
|
AnalyticsScreen(viewModel = viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Screen.Gyms> {
|
composable<Screen.Gyms> {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
fabConfig = FabConfig(
|
fabConfig =
|
||||||
|
FabConfig(
|
||||||
icon = Icons.Default.Add,
|
icon = Icons.Default.Add,
|
||||||
contentDescription = "Add Gym",
|
contentDescription = "Add Gym",
|
||||||
onClick = {
|
onClick = { navController.navigate(Screen.AddEditGym()) }
|
||||||
navController.navigate(Screen.AddEditGym())
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
GymsScreen(
|
GymsScreen(
|
||||||
@@ -139,27 +275,26 @@ fun OpenClimbApp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composable<Screen.Settings> {
|
composable<Screen.Settings> {
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) { fabConfig = null }
|
||||||
fabConfig = null // No FAB for settings
|
|
||||||
}
|
|
||||||
SettingsScreen(viewModel = viewModel)
|
SettingsScreen(viewModel = viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detail screens
|
|
||||||
composable<Screen.SessionDetail> { backStackEntry ->
|
composable<Screen.SessionDetail> { backStackEntry ->
|
||||||
val args = backStackEntry.toRoute<Screen.SessionDetail>()
|
val args = backStackEntry.toRoute<Screen.SessionDetail>()
|
||||||
|
LaunchedEffect(Unit) { fabConfig = null }
|
||||||
SessionDetailScreen(
|
SessionDetailScreen(
|
||||||
sessionId = args.sessionId,
|
sessionId = args.sessionId,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onNavigateToEdit = { sessionId ->
|
onNavigateToProblemDetail = { problemId ->
|
||||||
navController.navigate(Screen.AddEditSession(sessionId = sessionId))
|
navController.navigate(Screen.ProblemDetail(problemId))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Screen.ProblemDetail> { backStackEntry ->
|
composable<Screen.ProblemDetail> { backStackEntry ->
|
||||||
val args = backStackEntry.toRoute<Screen.ProblemDetail>()
|
val args = backStackEntry.toRoute<Screen.ProblemDetail>()
|
||||||
|
LaunchedEffect(Unit) { fabConfig = null }
|
||||||
ProblemDetailScreen(
|
ProblemDetailScreen(
|
||||||
problemId = args.problemId,
|
problemId = args.problemId,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
@@ -172,19 +307,26 @@ fun OpenClimbApp() {
|
|||||||
|
|
||||||
composable<Screen.GymDetail> { backStackEntry ->
|
composable<Screen.GymDetail> { backStackEntry ->
|
||||||
val args = backStackEntry.toRoute<Screen.GymDetail>()
|
val args = backStackEntry.toRoute<Screen.GymDetail>()
|
||||||
|
LaunchedEffect(Unit) { fabConfig = null }
|
||||||
GymDetailScreen(
|
GymDetailScreen(
|
||||||
gymId = args.gymId,
|
gymId = args.gymId,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onNavigateToEdit = { gymId ->
|
onNavigateToEdit = { gymId ->
|
||||||
navController.navigate(Screen.AddEditGym(gymId = gymId))
|
navController.navigate(Screen.AddEditGym(gymId = gymId))
|
||||||
|
},
|
||||||
|
onNavigateToSessionDetail = { sessionId ->
|
||||||
|
navController.navigate(Screen.SessionDetail(sessionId))
|
||||||
|
},
|
||||||
|
onNavigateToProblemDetail = { problemId ->
|
||||||
|
navController.navigate(Screen.ProblemDetail(problemId))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
composable<Screen.AddEditGym> { backStackEntry ->
|
composable<Screen.AddEditGym> { backStackEntry ->
|
||||||
val args = backStackEntry.toRoute<Screen.AddEditGym>()
|
val args = backStackEntry.toRoute<Screen.AddEditGym>()
|
||||||
|
LaunchedEffect(Unit) { fabConfig = null }
|
||||||
AddEditGymScreen(
|
AddEditGymScreen(
|
||||||
gymId = args.gymId,
|
gymId = args.gymId,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
@@ -194,6 +336,7 @@ fun OpenClimbApp() {
|
|||||||
|
|
||||||
composable<Screen.AddEditProblem> { backStackEntry ->
|
composable<Screen.AddEditProblem> { backStackEntry ->
|
||||||
val args = backStackEntry.toRoute<Screen.AddEditProblem>()
|
val args = backStackEntry.toRoute<Screen.AddEditProblem>()
|
||||||
|
LaunchedEffect(Unit) { fabConfig = null }
|
||||||
AddEditProblemScreen(
|
AddEditProblemScreen(
|
||||||
problemId = args.problemId,
|
problemId = args.problemId,
|
||||||
gymId = args.gymId,
|
gymId = args.gymId,
|
||||||
@@ -204,6 +347,7 @@ fun OpenClimbApp() {
|
|||||||
|
|
||||||
composable<Screen.AddEditSession> { backStackEntry ->
|
composable<Screen.AddEditSession> { backStackEntry ->
|
||||||
val args = backStackEntry.toRoute<Screen.AddEditSession>()
|
val args = backStackEntry.toRoute<Screen.AddEditSession>()
|
||||||
|
LaunchedEffect(Unit) { fabConfig = null }
|
||||||
AddEditSessionScreen(
|
AddEditSessionScreen(
|
||||||
sessionId = args.sessionId,
|
sessionId = args.sessionId,
|
||||||
gymId = args.gymId,
|
gymId = args.gymId,
|
||||||
@@ -212,6 +356,18 @@ fun OpenClimbApp() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notification permission dialog
|
||||||
|
if (showNotificationPermissionDialog) {
|
||||||
|
NotificationPermissionDialog(
|
||||||
|
onDismiss = { showNotificationPermissionDialog = false },
|
||||||
|
onRequestPermission = {
|
||||||
|
permissionLauncher.launch(
|
||||||
|
NotificationPermissionUtils.getNotificationPermissionString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +378,8 @@ fun OpenClimbBottomNavigation(navController: NavHostController) {
|
|||||||
|
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
bottomNavigationItems.forEach { item ->
|
bottomNavigationItems.forEach { item ->
|
||||||
val isSelected = when (item.screen) {
|
val isSelected =
|
||||||
|
when (item.screen) {
|
||||||
is Screen.Sessions -> currentRoute?.contains("Session") == true
|
is Screen.Sessions -> currentRoute?.contains("Session") == true
|
||||||
is Screen.Problems -> currentRoute?.contains("Problem") == true
|
is Screen.Problems -> currentRoute?.contains("Problem") == true
|
||||||
is Screen.Gyms -> currentRoute?.contains("Gym") == true
|
is Screen.Gyms -> currentRoute?.contains("Gym") == true
|
||||||
@@ -238,9 +395,7 @@ fun OpenClimbBottomNavigation(navController: NavHostController) {
|
|||||||
onClick = {
|
onClick = {
|
||||||
navController.navigate(item.screen) {
|
navController.navigate(item.screen) {
|
||||||
// Clear the entire back stack and go to the selected tab's root screen
|
// Clear the entire back stack and go to the selected tab's root screen
|
||||||
popUpTo(0) {
|
popUpTo(0) { inclusive = true }
|
||||||
inclusive = true
|
|
||||||
}
|
|
||||||
// Avoid multiple copies of the same destination when
|
// Avoid multiple copies of the same destination when
|
||||||
// reselecting the same item
|
// reselecting the same item
|
||||||
launchSingleTop = true
|
launchSingleTop = true
|
||||||
@@ -258,5 +413,3 @@ data class FabConfig(
|
|||||||
val contentDescription: String,
|
val contentDescription: String,
|
||||||
val onClick: () -> Unit
|
val onClick: () -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.atridad.openclimb.ui.components
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
@@ -13,6 +12,8 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.atridad.openclimb.data.model.ClimbSession
|
import com.atridad.openclimb.data.model.ClimbSession
|
||||||
import com.atridad.openclimb.data.model.Gym
|
import com.atridad.openclimb.data.model.Gym
|
||||||
|
import com.atridad.openclimb.ui.theme.CustomIcons
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
@@ -24,6 +25,16 @@ fun ActiveSessionBanner(
|
|||||||
onEndSession: () -> Unit
|
onEndSession: () -> Unit
|
||||||
) {
|
) {
|
||||||
if (activeSession != null) {
|
if (activeSession != null) {
|
||||||
|
// Add a timer that updates every second for real-time duration counting
|
||||||
|
var currentTime by remember { mutableStateOf(LocalDateTime.now()) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while (true) {
|
||||||
|
delay(1000) // Update every second
|
||||||
|
currentTime = LocalDateTime.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -67,7 +78,7 @@ fun ActiveSessionBanner(
|
|||||||
)
|
)
|
||||||
|
|
||||||
activeSession.startTime?.let { startTime ->
|
activeSession.startTime?.let { startTime ->
|
||||||
val duration = calculateDuration(startTime)
|
val duration = calculateDuration(startTime, currentTime)
|
||||||
Text(
|
Text(
|
||||||
text = duration,
|
text = duration,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
@@ -84,7 +95,7 @@ fun ActiveSessionBanner(
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Close,
|
imageVector = CustomIcons.Stop(MaterialTheme.colorScheme.onError),
|
||||||
contentDescription = "End session"
|
contentDescription = "End session"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -93,18 +104,18 @@ fun ActiveSessionBanner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateDuration(startTimeString: String): String {
|
private fun calculateDuration(startTimeString: String, currentTime: LocalDateTime): String {
|
||||||
return try {
|
return try {
|
||||||
val startTime = LocalDateTime.parse(startTimeString)
|
val startTime = LocalDateTime.parse(startTimeString)
|
||||||
val now = LocalDateTime.now()
|
val totalSeconds = ChronoUnit.SECONDS.between(startTime, currentTime)
|
||||||
val minutes = ChronoUnit.MINUTES.between(startTime, now)
|
val hours = totalSeconds / 3600
|
||||||
val hours = minutes / 60
|
val minutes = (totalSeconds % 3600) / 60
|
||||||
val remainingMinutes = minutes % 60
|
val seconds = totalSeconds % 60
|
||||||
|
|
||||||
when {
|
when {
|
||||||
hours > 0 -> "${hours}h ${remainingMinutes}m"
|
hours > 0 -> "${hours}h ${minutes}m ${seconds}s"
|
||||||
remainingMinutes > 0 -> "${remainingMinutes}m"
|
minutes > 0 -> "${minutes}m ${seconds}s"
|
||||||
else -> "< 1m"
|
else -> "${totalSeconds}s"
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
"Active"
|
"Active"
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
package com.atridad.openclimb.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.*
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.text.TextMeasurer
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.drawText
|
||||||
|
import androidx.compose.ui.text.rememberTextMeasurer
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data point for the line chart
|
||||||
|
*/
|
||||||
|
data class ChartDataPoint(
|
||||||
|
val x: Float,
|
||||||
|
val y: Float,
|
||||||
|
val label: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for chart styling
|
||||||
|
*/
|
||||||
|
data class ChartStyle(
|
||||||
|
val lineColor: Color,
|
||||||
|
val fillColor: Color,
|
||||||
|
val lineWidth: Float = 3f,
|
||||||
|
val gridColor: Color,
|
||||||
|
val textColor: Color,
|
||||||
|
val backgroundColor: Color
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Line Chart with area fill below the line
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LineChart(
|
||||||
|
data: List<ChartDataPoint>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
style: ChartStyle = ChartStyle(
|
||||||
|
lineColor = MaterialTheme.colorScheme.primary,
|
||||||
|
fillColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
|
||||||
|
gridColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
|
||||||
|
textColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.surface
|
||||||
|
),
|
||||||
|
showGrid: Boolean = true,
|
||||||
|
xAxisFormatter: (Float) -> String = { it.toString() },
|
||||||
|
yAxisFormatter: (Float) -> String = { it.toString() }
|
||||||
|
) {
|
||||||
|
val textMeasurer = rememberTextMeasurer()
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
if (data.isEmpty()) return@Canvas
|
||||||
|
|
||||||
|
val padding = with(density) { 32.dp.toPx() }
|
||||||
|
val chartWidth = size.width - padding * 2
|
||||||
|
val chartHeight = size.height - padding * 2
|
||||||
|
|
||||||
|
// Calculate data bounds
|
||||||
|
val dataMinY = data.minOf { it.y }
|
||||||
|
val dataMaxY = data.maxOf { it.y }
|
||||||
|
|
||||||
|
// Add some padding to Y-axis (10% above and below the data range)
|
||||||
|
val yPadding = if (dataMaxY == dataMinY) 1f else (dataMaxY - dataMinY) * 0.1f
|
||||||
|
val minY = dataMinY - yPadding
|
||||||
|
val maxY = dataMaxY + yPadding
|
||||||
|
|
||||||
|
val minX = data.minOf { it.x }
|
||||||
|
val maxX = data.maxOf { it.x }
|
||||||
|
|
||||||
|
val xRange = if (maxX - minX == 0f) 1f else maxX - minX // Minimum range of 1 for single points
|
||||||
|
val yRange = maxY - minY
|
||||||
|
|
||||||
|
// Ensure we have valid ranges
|
||||||
|
if (yRange == 0f) return@Canvas
|
||||||
|
|
||||||
|
// Convert data points to screen coordinates
|
||||||
|
val screenPoints = data.map { point ->
|
||||||
|
val x = padding + (point.x - minX) / xRange * chartWidth
|
||||||
|
val y = padding + chartHeight - (point.y - minY) / yRange * chartHeight
|
||||||
|
Offset(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
drawRect(
|
||||||
|
color = style.backgroundColor,
|
||||||
|
topLeft = Offset(padding, padding),
|
||||||
|
size = androidx.compose.ui.geometry.Size(chartWidth, chartHeight)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw grid
|
||||||
|
if (showGrid) {
|
||||||
|
drawGrid(
|
||||||
|
padding = padding,
|
||||||
|
chartWidth = chartWidth,
|
||||||
|
chartHeight = chartHeight,
|
||||||
|
gridColor = style.gridColor,
|
||||||
|
minX = minX,
|
||||||
|
maxX = maxX,
|
||||||
|
minY = minY,
|
||||||
|
maxY = maxY,
|
||||||
|
textMeasurer = textMeasurer,
|
||||||
|
textColor = style.textColor,
|
||||||
|
xAxisFormatter = xAxisFormatter,
|
||||||
|
yAxisFormatter = yAxisFormatter,
|
||||||
|
actualDataPoints = data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw area fill
|
||||||
|
if (screenPoints.size > 1) {
|
||||||
|
drawAreaFill(
|
||||||
|
points = screenPoints,
|
||||||
|
padding = padding,
|
||||||
|
chartHeight = chartHeight,
|
||||||
|
fillColor = style.fillColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw line
|
||||||
|
if (screenPoints.size > 1) {
|
||||||
|
drawLine(
|
||||||
|
points = screenPoints,
|
||||||
|
lineColor = style.lineColor,
|
||||||
|
lineWidth = style.lineWidth
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw data points - more pronounced
|
||||||
|
screenPoints.forEach { point ->
|
||||||
|
// Draw outer circle (larger)
|
||||||
|
drawCircle(
|
||||||
|
color = style.lineColor,
|
||||||
|
radius = 8f,
|
||||||
|
center = point
|
||||||
|
)
|
||||||
|
// Draw inner circle (white center)
|
||||||
|
drawCircle(
|
||||||
|
color = style.backgroundColor,
|
||||||
|
radius = 5f,
|
||||||
|
center = point
|
||||||
|
)
|
||||||
|
// Draw border for better visibility
|
||||||
|
drawCircle(
|
||||||
|
color = style.lineColor,
|
||||||
|
radius = 8f,
|
||||||
|
center = point,
|
||||||
|
style = Stroke(width = 2f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DrawScope.drawGrid(
|
||||||
|
padding: Float,
|
||||||
|
chartWidth: Float,
|
||||||
|
chartHeight: Float,
|
||||||
|
gridColor: Color,
|
||||||
|
minX: Float,
|
||||||
|
maxX: Float,
|
||||||
|
minY: Float,
|
||||||
|
maxY: Float,
|
||||||
|
textMeasurer: TextMeasurer,
|
||||||
|
textColor: Color,
|
||||||
|
xAxisFormatter: (Float) -> String,
|
||||||
|
yAxisFormatter: (Float) -> String,
|
||||||
|
actualDataPoints: List<ChartDataPoint>
|
||||||
|
) {
|
||||||
|
val textStyle = TextStyle(
|
||||||
|
color = textColor,
|
||||||
|
fontSize = 10.sp
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw vertical grid lines (X-axis) - only at integer values for sessions
|
||||||
|
val xRange = maxX - minX
|
||||||
|
if (xRange > 0) {
|
||||||
|
val startX = kotlin.math.ceil(minX).toInt()
|
||||||
|
val endX = kotlin.math.floor(maxX).toInt()
|
||||||
|
|
||||||
|
for (sessionNum in startX..endX) {
|
||||||
|
val x = padding + (sessionNum.toFloat() - minX) / xRange * chartWidth
|
||||||
|
|
||||||
|
// Draw grid line
|
||||||
|
drawLine(
|
||||||
|
color = gridColor,
|
||||||
|
start = Offset(x, padding),
|
||||||
|
end = Offset(x, padding + chartHeight),
|
||||||
|
strokeWidth = 1.dp.toPx()
|
||||||
|
)
|
||||||
|
|
||||||
|
// X-axis labels removed per user request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw horizontal grid lines (Y-axis) - only at actual data point values
|
||||||
|
val yRange = maxY - minY
|
||||||
|
if (yRange > 0) {
|
||||||
|
// Get unique Y values from actual data points
|
||||||
|
val actualYValues = actualDataPoints.map { kotlin.math.round(it.y).toInt() }.toSet()
|
||||||
|
|
||||||
|
actualYValues.forEach { gradeValue ->
|
||||||
|
val y = padding + chartHeight - (gradeValue.toFloat() - minY) / yRange * chartHeight
|
||||||
|
|
||||||
|
// Only draw if within chart bounds
|
||||||
|
if (y >= padding && y <= padding + chartHeight) {
|
||||||
|
// Draw grid line
|
||||||
|
drawLine(
|
||||||
|
color = gridColor,
|
||||||
|
start = Offset(padding, y),
|
||||||
|
end = Offset(padding + chartWidth, y),
|
||||||
|
strokeWidth = 1.dp.toPx()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw label
|
||||||
|
val text = yAxisFormatter(gradeValue.toFloat())
|
||||||
|
val textSize = textMeasurer.measure(text, textStyle)
|
||||||
|
drawText(
|
||||||
|
textMeasurer = textMeasurer,
|
||||||
|
text = text,
|
||||||
|
style = textStyle,
|
||||||
|
topLeft = Offset(
|
||||||
|
padding - textSize.size.width - 8.dp.toPx(),
|
||||||
|
y - textSize.size.height / 2f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DrawScope.drawAreaFill(
|
||||||
|
points: List<Offset>,
|
||||||
|
padding: Float,
|
||||||
|
chartHeight: Float,
|
||||||
|
fillColor: Color
|
||||||
|
) {
|
||||||
|
val bottomY = padding + chartHeight // This represents the bottom of the chart area
|
||||||
|
|
||||||
|
val path = Path().apply {
|
||||||
|
// Start from bottom-left (at chart bottom level)
|
||||||
|
moveTo(points.first().x, bottomY)
|
||||||
|
|
||||||
|
// Draw to first point
|
||||||
|
lineTo(points.first().x, points.first().y)
|
||||||
|
|
||||||
|
// Draw line through all points
|
||||||
|
for (i in 1 until points.size) {
|
||||||
|
lineTo(points[i].x, points[i].y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the path by going to bottom-right (at chart bottom level) and back to start
|
||||||
|
lineTo(points.last().x, bottomY)
|
||||||
|
lineTo(points.first().x, bottomY)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPath(
|
||||||
|
path = path,
|
||||||
|
color = fillColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DrawScope.drawLine(
|
||||||
|
points: List<Offset>,
|
||||||
|
lineColor: Color,
|
||||||
|
lineWidth: Float
|
||||||
|
) {
|
||||||
|
val path = Path().apply {
|
||||||
|
moveTo(points.first().x, points.first().y)
|
||||||
|
for (i in 1 until points.size) {
|
||||||
|
lineTo(points[i].x, points[i].y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawPath(
|
||||||
|
path = path,
|
||||||
|
color = lineColor,
|
||||||
|
style = Stroke(
|
||||||
|
width = lineWidth,
|
||||||
|
cap = StrokeCap.Round,
|
||||||
|
join = StrokeJoin.Round
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user