diff --git a/AndroidManifest.xml b/AndroidManifest.xml new file mode 100644 index 0000000..3e87950 --- /dev/null +++ b/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE index 472ac23..82a3cf8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ MIT License -Copyright (c) +Copyright (c) 2018 xdrm-brackets (Adrien Marquès) & SeekDaSky (Lucas Mascaro) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/ic_launcher_default-web.png b/ic_launcher_default-web.png new file mode 100644 index 0000000..1838a6c Binary files /dev/null and b/ic_launcher_default-web.png differ diff --git a/ic_splash_icon-web.png b/ic_splash_icon-web.png new file mode 100644 index 0000000..a406878 Binary files /dev/null and b/ic_splash_icon-web.png differ diff --git a/ic_splash_icon_bg-web.png b/ic_splash_icon_bg-web.png new file mode 100644 index 0000000..1838a6c Binary files /dev/null and b/ic_splash_icon_bg-web.png differ diff --git a/java/io/xdrm/lebonprix/GooeySplashScreen.kt b/java/io/xdrm/lebonprix/GooeySplashScreen.kt new file mode 100644 index 0000000..d1ece3a --- /dev/null +++ b/java/io/xdrm/lebonprix/GooeySplashScreen.kt @@ -0,0 +1,47 @@ +package io.xdrm.lebonprix + +import android.content.Context +import android.content.Intent +import android.media.AudioManager +import android.media.MediaPlayer +import android.support.v7.app.AppCompatActivity +import android.os.Bundle +import android.os.Handler +import android.provider.MediaStore +import android.view.WindowManager +import io.xdrm.lebonprix.anim.GooeySplashAnimation +import kotlinx.android.synthetic.main.activity_gooey_splash_screen.* +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch + +class GooeySplashScreen : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + setContentView(R.layout.activity_gooey_splash_screen) + + // 1. launch animation + val anim = GooeySplashAnimation(resources, canvas_wrapper) + anim.duration = 4000 + anim.start() + + // 2. Override media volume if below a required level + val audio = getSystemService(Context.AUDIO_SERVICE) as AudioManager + val requiredMinimumVolume = audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC) * 0.5 + + if( audio.getStreamVolume(AudioManager.STREAM_MUSIC) < requiredMinimumVolume ) + audio.setStreamVolume(AudioManager.STREAM_MUSIC, requiredMinimumVolume.toInt(), 0) + + // 3. play sound + override volume + val player = MediaPlayer.create(this, R.raw.drop) + Handler().postDelayed({ player.start() }, 1000) + + // 4. go to Home after delay + Handler().postDelayed({ + player.stop() + startActivity( Intent(this, HomeActivity::class.java) ) + finish() + }, 4000) + } +} diff --git a/java/io/xdrm/lebonprix/HomeActivity.kt b/java/io/xdrm/lebonprix/HomeActivity.kt new file mode 100644 index 0000000..d710fe1 --- /dev/null +++ b/java/io/xdrm/lebonprix/HomeActivity.kt @@ -0,0 +1,231 @@ +package io.xdrm.lebonprix + +import android.content.Intent +import android.support.v7.app.AppCompatActivity +import android.os.Bundle +import android.os.Handler +import android.text.Editable +import android.text.TextWatcher +import android.util.Log +import android.view.WindowManager +import android.widget.Toast +import io.xdrm.lebonprix.anim.HomeFilterAnimation +import io.xdrm.lebonprix.anim.UnderlineAnimation +import io.xdrm.lebonprix.api.CategoryFetcher +import io.xdrm.lebonprix.api.PricesFetcher +import io.xdrm.lebonprix.model.Category +import io.xdrm.lebonprix.model.CategoryItem +import io.xdrm.lebonprix.model.CategoryItemStore +import io.xdrm.todoleast.adapter.CategoryAdapter +import kotlinx.android.synthetic.main.activity_home.* +import kotlinx.coroutines.* +import okhttp3.OkHttpClient +import org.json.JSONArray + + + +class HomeActivity : AppCompatActivity() { + + companion object { + val SEARCH_KEY_TIMEOUT: Long = 500 + } + + private val categoryStore = CategoryItemStore + private val categoryAdapter = CategoryAdapter(CategoryItemStore.data) + + private lateinit var underline_animator: UnderlineAnimation + private lateinit var load_animator: HomeFilterAnimation + + private var last_search_time: Long = 0 + private var last_search: String = "" + private var httpJob: Job? = null + + private val httpClient = OkHttpClient() + private var ended = false + private var loader: Job? = null + + private lateinit var apiCategories: CategoryFetcher + private lateinit var apiPrices: PricesFetcher + + override fun onResume() { + super.onResume() + runOnUiThread { load_animator.stage(HomeFilterAnimation.CLEAR_ALL).during(0).start() } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + setContentView(R.layout.activity_home) + + // manage underline animation + underline_animator = UnderlineAnimation(search_underline) + load_animator = HomeFilterAnimation(applicationContext, filter) + apiCategories = CategoryFetcher(this, search_underline, underline_animator, httpClient) + apiPrices = PricesFetcher(this, httpClient) + + // listen for focus changes + search.setOnFocusChangeListener{v, focus -> + if( focus ) underline_animator.animate(0F, 1F).during(30).start() + else underline_animator.animate(1F, 0F).during(30).start() + } + + // listen for clicks + search.setOnClickListener { + underline_animator.animate(0F, 1F).during(30).start() + } + + category_grid.adapter = categoryAdapter + + // launch category fetch + search.addTextChangedListener(object : TextWatcher{ + override fun afterTextChanged(s: Editable?) {} + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + categoryCoordinator(search.text.toString().trim()) + + // manage when key is pressed before the SEARCH_KEY_TIMEOUT is reached, so ignored + Handler().postDelayed({ + categoryCoordinator(search.text.toString().trim()) + }, HomeActivity.SEARCH_KEY_TIMEOUT) + } + }) + + + // launch search + search_button.setOnClickListener { + val selectedCategories = getCategories() + + // (1) Loader + if( loader != null && loader!!.isActive ) + loader!!.cancel() + + loader = GlobalScope.launch(Dispatchers.Main) { + // launch animation + load_animator.stage(HomeFilterAnimation.STAGE_CLEAN).during(300).start() + delay(300) + load_animator.stage(HomeFilterAnimation.STAGE_CENTER).during(300).start() + delay(300) + while(true){ + load_animator.stage(HomeFilterAnimation.STAGE_LOAD).during(5000).start() + delay(5000) + } + } + + if( httpJob != null && httpJob!!.isActive ) + httpJob!!.cancel() + + httpJob = GlobalScope.launch { + + // exec request + val prices = apiPrices.fetch(search.text.toString(), selectedCategories.toTypedArray()) + loader?.cancel() + + // stop all if no result + if( prices.isNullOrEmpty() ) { + runOnUiThread { + load_animator.stage(HomeFilterAnimation.CLEAR_ALL).during(0).start() + } + return@launch + } + + // transition + runOnUiThread { + load_animator.stage(HomeFilterAnimation.STAGE_END).during(500).start() + } + delay(500) + + val int = Intent(this@HomeActivity, ResultActivity::class.java) + int.putExtra("prices", prices) + startActivity(int) + overridePendingTransition(0, 0) + } + + } + + updateCategories(JSONArray("[]")) + + } + + private fun categoryCoordinator(keywords: String){ + val now = System.currentTimeMillis() + if( last_search_time > 0 && now-last_search_time < HomeActivity.SEARCH_KEY_TIMEOUT ) + return + + if( keywords == last_search ) + return + + last_search_time = now + last_search = keywords + + if( httpJob != null && httpJob!!.isActive ) { + httpJob!!.cancel() + underline_animator.animateContinue(1F).during(0).start() + } + + httpJob = GlobalScope.launch { + updateCategories( apiCategories.fetch(last_search) ) + } + } + + + private fun updateCategories(categories: JSONArray){ + // 1. Update categories + categoryStore.data.clear() + categoryStore.data.add(CategoryItem(Category.ALL, false, 1F)) // preselected + var total = 0 + + for( i in 0..categories.length() ){ + // extract features + var label: String = "" + var count: Int = 0 + try { + label = categories.getJSONArray(i).getString(0) + count = categories.getJSONArray(i).getInt(1) + }catch(e: Exception){ continue } + + total += count + + // try to parse + val cat: Category = Category.fromLabel(label) ?: continue + + // add category + categoryStore.data.add(CategoryItem(cat, false, count.toFloat())) + + // progress feedback + val progress = 0.5F + 0.5*i/categories.length() + runOnUiThread{ underline_animator.animateContinue(progress.toFloat()).during(200).start() } + } + + for( i in 1..categories.length() ){ + categoryStore.data[i].count /= total + } + + // 3. update adapter + runOnUiThread{ + categoryAdapter.notifyDataSetChanged() + underline_animator.animateContinue(1F).during(10).start() + } + } + + + private fun getCategories() : MutableList { + + val list = mutableListOf() + + for( item in categoryStore.data ){ + // not selected -> ignore + if( !item.selected ) + continue; + + // select all -> return empty + if( item.category == Category.ALL ) + return mutableListOf() + + // else, add each one + list.add(item.category.label) + } + + return list + } +} diff --git a/java/io/xdrm/lebonprix/ResultActivity.kt b/java/io/xdrm/lebonprix/ResultActivity.kt new file mode 100644 index 0000000..0ff75a3 --- /dev/null +++ b/java/io/xdrm/lebonprix/ResultActivity.kt @@ -0,0 +1,70 @@ +package io.xdrm.lebonprix + +import android.support.v7.app.AppCompatActivity +import android.os.Bundle +import android.os.Handler +import android.util.Log +import android.view.WindowManager +import io.xdrm.lebonprix.anim.BarChart +import io.xdrm.lebonprix.anim.ResultOpeningAnimation +import io.xdrm.lebonprix.anim.ResultRangeAnimation +import kotlinx.android.synthetic.main.activity_result.* + +class ResultActivity : AppCompatActivity() { + + private var results: Array? = arrayOf() + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + setContentView(R.layout.activity_result) + + // show results form calling activity (intent) + results = intent?.extras?.getSerializable("prices") as Array + + // show opening transition + val anim = ResultOpeningAnimation(applicationContext, filter) + anim.duration = 500 + anim.start() + + // show results after animation + Handler().postDelayed({ + showResultRange(result_range.measuredWidth, result_range.measuredHeight) + showResultChart(result_chart.measuredWidth, result_chart.measuredHeight) + + // bind on layout changes + result_range.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> + showResultRange(right-left, bottom-top) + } + result_chart.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> + showResultChart(right-left, bottom-top) + } + }, 300) + + } + + private fun showResultRange(width: Int, height: Int) { + if( results.isNullOrEmpty() ) + return + + // (1) slider range + val slider = ResultRangeAnimation(this, result_range, width, height) + slider.setData(results!!.size, results!!.min()!!.toFloat(), results!!.average().toFloat(), results!!.max()!!.toFloat()) + slider.duration = 1000 + slider.start() + + } + + private fun showResultChart(width: Int, height: Int){ + if( results.isNullOrEmpty() ) + return + + // (1) bar chart + val chart = BarChart(results!!, width, height, applicationContext) + result_chart.setImageDrawable(chart) + + + + } +} diff --git a/java/io/xdrm/lebonprix/adapter/CategoryAdapter.kt b/java/io/xdrm/lebonprix/adapter/CategoryAdapter.kt new file mode 100644 index 0000000..3ab7131 --- /dev/null +++ b/java/io/xdrm/lebonprix/adapter/CategoryAdapter.kt @@ -0,0 +1,82 @@ +package io.xdrm.todoleast.adapter + + +import android.graphics.PorterDuff +import android.opengl.Visibility +import android.support.v4.content.res.ResourcesCompat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import io.xdrm.lebonprix.R +import io.xdrm.lebonprix.model.Category +import io.xdrm.lebonprix.model.CategoryItem +import kotlinx.android.synthetic.main.category_item.view.* +import kotlin.math.round + +class CategoryAdapter(private val dataSet: List) : BaseAdapter() { + + + override fun getView(i: Int, convertView: View?, parent: ViewGroup) : View?{ + if( i >= dataSet.size ) + return null + + val item = dataSet[i] + + // inflate + val repr: View = LayoutInflater.from(parent.context).inflate(R.layout.category_item, parent, false) + + // 1. status (selected or not) + repr.selected.isChecked = item.selected + repr.setOnClickListener { view -> + item.selected = !item.selected + view.selected.setChecked(item.selected) + + if( view.selected.isChecked ) setSelected(view) + else setUnselected(view) + } + + // 2. label + repr.label.text = item.category.label + + // 3. count + repr.count.text = round(item.count*100).toInt().toString() + " %" + + // hide if "ALL" + if( item.category == Category.ALL ) + repr.count.visibility = View.INVISIBLE + + // 4. infer icon + val category = Category.fromLabel(item.category.label) + if( category != null ) + repr.icon.setImageResource(category!!.iconId) + + // 5. manage selected or not + if( item.selected ) setSelected(repr) + else setUnselected(repr) + + return repr + + } + + + override fun getCount() = dataSet.size + override fun getItem(i: Int) = dataSet[i] + override fun getItemId(i: Int) = i.toLong() + + + private fun setSelected(v: View){ + val whiteColor = ResourcesCompat.getColor(v.resources, android.R.color.white, null) + v.setBackgroundResource(R.drawable.rounded_category_item_bg_selected) + v.label.setTextColor(whiteColor) + v.icon.setColorFilter(0xffffffff.toInt(), PorterDuff.Mode.SRC_IN) + } + + private fun setUnselected(v: View){ + v.setBackgroundResource(R.drawable.rounded_category_item_bg) + val blackColor = ResourcesCompat.getColor(v.resources, android.R.color.black, null) + v.label.setTextColor(blackColor) + v.icon.clearColorFilter() + } + +} \ No newline at end of file diff --git a/java/io/xdrm/lebonprix/anim/BarChart.kt b/java/io/xdrm/lebonprix/anim/BarChart.kt new file mode 100644 index 0000000..6f70fc4 --- /dev/null +++ b/java/io/xdrm/lebonprix/anim/BarChart.kt @@ -0,0 +1,183 @@ +package io.xdrm.lebonprix.anim + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.* +import android.graphics.drawable.Drawable +import android.support.v4.content.res.ResourcesCompat +import android.util.Log +import android.view.animation.DecelerateInterpolator +import io.xdrm.lebonprix.R +import kotlin.math.* + +class BarChart( + data : Array, + private var originalWidth : Int, + private val originalHeight : Int, + val ctx: Context +) : Drawable() { + + // PARAMS + private val relativeMinBarSize = 0.01F + private val relativeSideMargin = 0.1F // only 1 side + private val relativeSpaceWidth = 1F // relative to bar size + + // fixed/processed values + private val minReadableSlotSize = 90F + private val textDistance = 120F + private val fontSize = 32F + private val sideMargin = originalWidth * 0.5F * relativeSideMargin + private val width = originalWidth - 2F*sideMargin + private val minBarSize = relativeMinBarSize * width + private val minSlotSize = minBarSize * (1F + relativeSpaceWidth) + // available width / (size of minimum bar + space) - ending space +// private val maxSlotCount = width / (minBarSize * (1F+relativeSpaceWidth) - minBarSize*relativeSpaceWidth) + + // calculated on the run + private var barSize: Float + private var slotSize: Float + + + // animation + private val animationDuration = 500L + private var animatedHeight = 0 + private val animator = ValueAnimator.ofInt(0, originalHeight) + + // price -> count + private val distributions = arrayListOf>() + + init{ + require( data.isNotEmpty() ) + + // get maximum number of bars + val barCount = data.distinct().size + val minPrice = data.min()!!.toFloat() + val maxPrice = data.max()!!.toFloat() + + // defaults + slotSize = max(width / barCount, minSlotSize ) + var slotCount = ( width / slotSize ).toInt() + // avail width / (elemts + 1 + 1 end space) + slotSize = width / (slotCount+1F-0.5F) + + + barSize = slotSize / (1F+relativeSpaceWidth) + var step = ( maxPrice - minPrice ) / slotCount.toFloat() + + // move between min and max incrementing by 'step' every time + var slot = minPrice + var sum = 0 + while( slot <= maxPrice ){ + val min = slot - step*0.5F + val max = slot + step*0.5F + + data.forEach { + // first slot -> inclusive min + if( slot <= minPrice && it >= min && it <= max ) + sum++ + + else if( it > min && it <= max ) + sum++ + } + + distributions.add( Pair(slot, sum) ) + sum = 0 + slot += step + } + + /** + * SETUP ANIMATION + */ + animator.duration = animationDuration + animator.interpolator = DecelerateInterpolator() + animator.addUpdateListener { + animatedHeight = it.animatedValue as Int + invalidateSelf() + } + animator.start() + } + + override fun draw(canvas: Canvas) { + //create base color (roboto light with gradient) + val gradientPaint = Paint() + gradientPaint.shader = LinearGradient(0F,0F, originalWidth.toFloat(), animatedHeight.toFloat(), 0xff71f0b5.toInt(), 0xff3e91e3.toInt(), Shader.TileMode.CLAMP) + + val textColor = Paint() + textColor.typeface = Typeface.create("sans-serif-light", Typeface.NORMAL) + textColor.textSize = this.fontSize + textColor.textAlign = Paint.Align.CENTER + textColor.color = Color.WHITE + textColor.flags = Paint.ANTI_ALIAS_FLAG + + // text background + val bg = Paint() + bg.color = ResourcesCompat.getColor(ctx.resources, R.color.colorBackground, null) + bg.flags = Paint.ANTI_ALIAS_FLAG + + canvas.drawRoundRect( + RectF(-50F, originalHeight-textDistance+10F, originalWidth+50F, originalHeight+textDistance+1F), + 100F, 100F, + bg + ) + + drawText(canvas, textColor) + + drawBars(canvas, gradientPaint) + + setBounds(0,0,originalWidth,animatedHeight) + } + + private fun drawText(canvas: Canvas, paint: Paint){ + distributions.forEachIndexed { i, value -> + + // get mod where text is printed 1 over mod times (exceptions: start, end) + val mod = max(1, (minReadableSlotSize / slotSize).toInt()) + + if( i == 0 || i == distributions.size-1 || i % mod == 0 ){ + + val x = sideMargin + i.toFloat() * slotSize + barSize/2F + val y = originalHeight.toFloat() + var text = "${ round(value.first) }" + if( text == "${value.first.toInt()}.0" ) + text = "${ value.first.toInt() }" + + + //save canvas, rotate, translate , draw, then restore to previous state + canvas.save() + canvas.rotate(-45F,x,y- textDistance*0.4F) + canvas.drawText(text,x,y - textDistance*0.4F, paint) + canvas.restore() + + } + + } + } + + private fun drawBars(canvas: Canvas, paint: Paint){ + val maxCount = distributions.flatMap { listOf(it.second.toFloat()) }.max()!! + val maxAnimatedHeight = animatedHeight - textDistance + + distributions.forEachIndexed { i, d -> + + // process coordinates + val x = sideMargin + i.toFloat() * slotSize + + val height = d.second.toFloat() / maxCount + val y = maxAnimatedHeight * (1 - height) + + canvas.drawRoundRect( + RectF(x, y, x+barSize, originalHeight.toFloat()-textDistance), + barSize, barSize, + paint) + + } + } + + override fun setAlpha(alpha: Int) {} + + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } + + override fun setColorFilter(colorFilter: ColorFilter?) {} +} \ No newline at end of file diff --git a/java/io/xdrm/lebonprix/anim/GooeySplashAnimation.kt b/java/io/xdrm/lebonprix/anim/GooeySplashAnimation.kt new file mode 100644 index 0000000..3de3e1e --- /dev/null +++ b/java/io/xdrm/lebonprix/anim/GooeySplashAnimation.kt @@ -0,0 +1,158 @@ +package io.xdrm.lebonprix.anim + +import android.content.res.Resources +import android.graphics.* +import android.graphics.drawable.Drawable +import android.support.v4.content.res.ResourcesCompat +import android.util.Log +import android.view.animation.* +import android.widget.ImageView +import io.xdrm.lebonprix.R + + +class GooeySplashAnimation(val res: Resources, val target: ImageView) : Animation() { + + private var width: Int = 1000 + private var height: Int = 1000 + + private var bitmap : Bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + private var canvas : Canvas = Canvas(bitmap) + + private var progress: Float = 0F; + + private var icon: Drawable? + + data class wave(var relrad: Float, var clockWise: Boolean) + private var waves: MutableList = mutableListOf() + + + + init { + icon = ResourcesCompat.getDrawable(res, R.mipmap.ic_splash_icon, null) + + target.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> + width = right-left + height = bottom-top + + // unregister previous + target.setImageBitmap(null) + canvas.setBitmap(null) + bitmap.recycle() + + // build new + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + canvas = Canvas(bitmap) + target.setImageBitmap(bitmap) + + generateWaves() + } + } + + + + override fun start() { + target.startAnimation(this) + } + + override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { + super.applyTransformation(interpolatedTime, t) + progress = interpolatedTime + draw() + } + + + + fun draw(){ + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + + drawWave() + + drawIcon() + + target.setImageBitmap(bitmap) + + } + + private fun drawIcon(){ + if( icon == null ) + return + + val bgSize = 280 + (20 + 20 * Math.sin(2*progress*Math.PI)).toInt() + val bgOffsetX = (width-bgSize)/2 + val bgOffsetY = (height-bgSize)/2 + + icon!!.setBounds( bgOffsetX, bgOffsetY , bgOffsetX+bgSize, bgOffsetY+bgSize) + icon!!.draw(canvas) + } + + private fun drawWave(){ + val easeProgress = DecelerateInterpolator().getInterpolation(progress) + + // -- PARAMETERS + // the wave will have a radius between 2% and -2% of the radius + val relativeRadiusVariation = 0.02F + val wavePerRevolution = 15 + val revolutions = 20 + val samples = 200 + val relativeExpansionStartRadius = 1F // radius to expand from (relative to radius) + + // -- PREPROCESSED VALUES + val slice = 2*Math.PI.toFloat() / samples; + val centerx = width * 0.5F + val centery = height * 0.5F + +// val gradient: Shader = RadialGradient(x, y, width*0.5F, 0x554e5dff, Color.TRANSPARENT, Shader.TileMode.CLAMP) +// val paint = Paint() +// paint.shader = gradient + val paint = Paint() + paint.style = Paint.Style.STROKE + paint.color = 0x3de0e5 + paint.alpha = 127 - (127*Math.sin(Math.PI*(2*progress + 0.5))).toInt() + paint.strokeWidth = width*0.003F * progress +// paint.flags = Paint.ANTI_ALIAS_FLAG + + + for( w in waves ){ + + val p = Path() + + for( i in 0..samples ) { + val angle = slice * i.toDouble() + var rotation = wavePerRevolution*angle + easeProgress*revolutions*Math.PI + if( w.clockWise ) rotation = wavePerRevolution*angle - easeProgress*revolutions*Math.PI + + // calculate proportional progress between minimum radius & end radius + val shiftedProgress = (progress + relativeExpansionStartRadius) / (1F+relativeExpansionStartRadius) + var rad = width*w.relrad * ( 1 + relativeRadiusVariation * (.2+progress*.8).toFloat() * Math.sin(rotation).toFloat() ) + + val x = centerx + rad*shiftedProgress * Math.cos(angle).toFloat() + val y = centery + rad*shiftedProgress * Math.sin(angle).toFloat() + + if( i == 0 ) p.moveTo(x, y) + else p.lineTo(x, y) + } + + canvas.drawPath(p, paint) + } + + +// val offset = ( width * progress ).toInt() +// +// wave1.setBounds(-(width-offset), (height*0.8).toInt(), width+offset, height) +// wave1.draw(canvas) +// +// wave2.setBounds(-offset, (height*0.7).toInt(), 2*width-offset, height) +// wave2.draw(canvas) + + } + + + private fun generateWaves(count: Int = 1){ + + for( i in 0..count ) + waves.add( wave(0.3F, true) ) + waves.add( wave(0.5F, false) ) + waves.add( wave(0.7F, true) ) + + } +} \ No newline at end of file diff --git a/java/io/xdrm/lebonprix/anim/HomeFilterAnimation.kt b/java/io/xdrm/lebonprix/anim/HomeFilterAnimation.kt new file mode 100644 index 0000000..2fb3024 --- /dev/null +++ b/java/io/xdrm/lebonprix/anim/HomeFilterAnimation.kt @@ -0,0 +1,216 @@ +package io.xdrm.lebonprix.anim + +import android.content.Context +import android.graphics.* +import android.graphics.drawable.Drawable +import android.support.v4.content.res.ResourcesCompat +import android.util.Log +import android.view.animation.Animation +import android.view.animation.Transformation +import android.widget.ImageView +import android.util.TypedValue +import io.xdrm.lebonprix.R + + +class HomeFilterAnimation( + val ctx: Context, + val target: ImageView +) : Animation(){ + + companion object { + // stage 1 : clean the page + val STAGE_CLEAN = 0 + // stage 2 : move to center + val STAGE_CENTER = 1 + // stage 3 : loading animation + val STAGE_LOAD = 2 + // stage 4 : clean the page for next activity + val STAGE_END = 3 + val CLEAR_ALL = 4 + } + + // PARAMS + private val dpIconSize: Int = 44 + private val dpIconTop: Int = 8 + private val dpIconRight: Int = 8 + + // FIXED/PROCESSED VALUES + private val iconSize: Float = dptopx(dpIconSize) + private val iconTop: Float = dptopx(dpIconTop) + private val iconRight: Float = dptopx(dpIconRight) + + + // VARIABLES + private var width: Int = 1000 + private var height: Int = 1000 + + private var bitmap : Bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + private var canvas : Canvas = Canvas(bitmap) + + private var icon: Drawable? + + // ANIMATION + private var stage: Int = HomeFilterAnimation.STAGE_CLEAN + private var progress: Float = 0F + + init { + icon = ResourcesCompat.getDrawable(ctx.resources, R.drawable.ic_search, null) + + target.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> + width = right-left + height = bottom-top + + // unregister previous + target.setImageBitmap(null) + canvas.setBitmap(null) + bitmap.recycle() + + // build new + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + canvas = Canvas(bitmap) + target.setImageBitmap(bitmap) + } + } + + + // Convert dp to pixels + private fun dptopx(dp: Int) : Float{ + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), ctx.resources.displayMetrics) + } + + fun stage(s: Int) : HomeFilterAnimation{ + stage = s + return this + } + + fun during(d: Long) : HomeFilterAnimation { + super.setDuration(d) + return this + } + + override fun start() { + target.startAnimation(this) + } + + override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { + super.applyTransformation(interpolatedTime, t) + progress = interpolatedTime + draw() + } + + private fun draw(){ + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + + // Clean the page + if( stage == HomeFilterAnimation.STAGE_CLEAN ) { + + drawPageCleaner(progress) + drawCenterIconBackground(0F) + drawCenterIcon(0F) + + } else if( stage == HomeFilterAnimation.STAGE_CENTER ){ + + drawPageCleaner(1F) + drawCenterIconBackground(progress) + drawCenterIcon(progress) + + } else if( stage == HomeFilterAnimation.STAGE_LOAD ){ + + drawPageCleaner(1F) + drawLoadingIconBackground(progress) + drawCenterIcon(1F) + + } else if( stage == HomeFilterAnimation.STAGE_END ){ + + drawPageCleaner(1F) + drawClearingIconBackground(progress) + drawCenterIcon(1F) + + } + + target.setImageBitmap(bitmap) + } + + private fun drawPageCleaner(prog: Float){ + val bg = Paint() + bg.color = Color.WHITE + + // max radius to hide the whole screen + val maxRad = Math.sqrt( Math.pow(width.toDouble(),2.toDouble()) + Math.pow(height.toDouble(), 2.toDouble())) * 1.5F // security of 1.5x the max radius + val rad = prog * maxRad + val x = width - iconRight - iconSize/2F + val y = iconTop + iconSize/2F + canvas.drawCircle(x, y, rad.toFloat(), bg) + } + private fun drawClearingIconBackground(prog: Float){ + val bg = Paint() + bg.color = ResourcesCompat.getColor(ctx.resources, R.color.colorBackground, null) + + // max radius to hide the whole screen + val maxRad = Math.sqrt( Math.pow(width.toDouble(),2.toDouble()) + Math.pow(height.toDouble(), 2.toDouble())) * 1.5F // security of 1.5x the max radius + val rad = prog * maxRad + + val x = width.toFloat() / 2F + val y = height.toFloat() / 2F + + canvas.drawCircle(x, y, rad.toFloat(), bg) + } + + private fun drawCenterIconBackground(prog: Float){ + val bg = Paint() + bg.color = ResourcesCompat.getColor(ctx.resources, R.color.colorBackground, null) + + val rad = iconSize/2F + + // get start / end position (end = centered) + val startx = width - iconRight - rad + val starty = iconTop + rad + + val endx = width.toFloat() / 2F + val endy = height.toFloat() / 2F + + val x = startx + prog * (endx - startx) + val y = starty + prog * (endy - starty) + + canvas.drawCircle(x, y, rad, bg) + } + + + private fun drawCenterIcon(prog: Float) { + if( icon == null ) + return + + // get start / end position (end = centered) + val startx = width - iconRight - iconSize + val starty = iconTop + + val endx = width.toFloat() / 2F - iconSize/2F + val endy = height.toFloat() / 2F - iconSize/2F + + + // get x,y from start position to center + val x = (startx + prog * (endx - startx)).toInt() + val y = (starty + prog * (endy - starty)).toInt() + + icon!!.setBounds(x, y, x + iconSize.toInt(), y + iconSize.toInt()) + icon!!.draw(canvas) + + } + + private fun drawLoadingIconBackground(prog: Float){ + val bg = Paint() + bg.color = ResourcesCompat.getColor(ctx.resources, R.color.colorBackground, null) + + var rad = iconSize/2F + // evoluting size + rad *= ( 1F + (0.5F + 0.5F * Math.sin((0.5+prog)*2*Math.PI).toFloat()) ) + + // centered + val x = width.toFloat() / 2F + val y = height.toFloat() / 2F + + canvas.drawCircle(x, y, rad, bg) + } + + +} \ No newline at end of file diff --git a/java/io/xdrm/lebonprix/anim/ResultOpeningAnimation.kt b/java/io/xdrm/lebonprix/anim/ResultOpeningAnimation.kt new file mode 100644 index 0000000..777a4ed --- /dev/null +++ b/java/io/xdrm/lebonprix/anim/ResultOpeningAnimation.kt @@ -0,0 +1,75 @@ +package io.xdrm.lebonprix.anim + +import android.content.Context +import android.graphics.* +import android.support.v4.content.res.ResourcesCompat +import android.view.animation.Animation +import android.view.animation.Transformation +import android.widget.ImageView +import io.xdrm.lebonprix.R + +class ResultOpeningAnimation( + val ctx: Context, + val target: ImageView +) : Animation() { + + + // VARIABLES + private var width: Int = 1000 + private var height: Int = 1000 + + private var bitmap : Bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + private var canvas : Canvas = Canvas(bitmap) + + private var progress: Float = 0F + + init { + target.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> + width = right-left + height = bottom-top + + // unregister previous + target.setImageBitmap(null) + canvas.setBitmap(null) + bitmap.recycle() + + // build new + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + canvas = Canvas(bitmap) + target.setImageBitmap(bitmap) + } + } + + override fun start() { + target.startAnimation(this) + } + + override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { + super.applyTransformation(interpolatedTime, t) + progress = interpolatedTime + draw() + } + + private fun draw(){ + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + + drawFilter() + + target.setImageBitmap(bitmap) + } + + private fun drawFilter(){ + val bg = Paint() + bg.color = ResourcesCompat.getColor(ctx.resources, R.color.colorBackground, null) + + // max radius to hide the whole screen + val maxRad = Math.sqrt( Math.pow(width.toDouble(),2.toDouble()) + Math.pow(height.toDouble(), 2.toDouble())) * 1.5F // security of 1.5x the max radius + var rad = (1F-progress) * maxRad + + // centered + val x = width.toFloat() / 2F + val y = height.toFloat() / 2F + + canvas.drawCircle(x, y, rad.toFloat(), bg) + } +} \ No newline at end of file diff --git a/java/io/xdrm/lebonprix/anim/ResultRangeAnimation.kt b/java/io/xdrm/lebonprix/anim/ResultRangeAnimation.kt new file mode 100644 index 0000000..df7ed51 --- /dev/null +++ b/java/io/xdrm/lebonprix/anim/ResultRangeAnimation.kt @@ -0,0 +1,247 @@ +package io.xdrm.lebonprix.anim + +import android.content.Context +import android.content.res.AssetManager +import android.content.res.Resources +import android.graphics.* +import android.support.v4.content.res.ResourcesCompat +import android.util.Log +import android.view.animation.Animation +import android.view.animation.BounceInterpolator +import android.view.animation.CycleInterpolator +import android.view.animation.Transformation +import android.widget.ImageView +import io.xdrm.lebonprix.R +import kotlin.math.round + +class ResultRangeAnimation( + private val ctx: Context, + private val target: ImageView, + private val originalWidth: Int, + private val originalHeight: Int +) : Animation() { + + + private var width: Int = 1000 + private var height: Int = 1000 + + private var bitmap : Bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + private var canvas : Canvas = Canvas(bitmap) + + private var progress: Float = 0F + private val relativeSliderWidth = 0.75F + private var w = 0F + private var h = 0F + + // actual data to display + private var size: Float = 0F + private var min: Float = 0F + private var avg: Float = 0F + private var max: Float = 0F + fun setData(_size: Int, _min: Float, _avg: Float, _max: Float){ + require(_avg > _min); require(_avg < _max) + + size = _size.toFloat() + min = _min; + avg = _avg; + max = _max + } + + init { + setSize(originalWidth, originalHeight) + + target.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom -> + setSize(right-left, bottom-top) + } + } + + override fun start() { + target.startAnimation(this) + } + + override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { + super.applyTransformation(interpolatedTime, t) + progress = interpolatedTime + draw() + } + + private fun setSize(_width: Int, _height: Int){ + width = _width + height = _height + + // unregister previous + target.setImageBitmap(null) + canvas.setBitmap(null) + bitmap.recycle() + + // build new + bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + canvas = Canvas(bitmap) + target.setImageBitmap(bitmap) + } + + + private fun draw(){ + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + + w = width * 1F + h = height * 0.6F + + drawSlider() + drawAverage() + + target.setImageBitmap(bitmap) + } + + + private fun drawSlider(){ + + // params + val sliderThickness = 5F + val sliderDotRadius = 10F + val relativeCaptionSize = 0.04F + val relativeValuesSize = 0.04F + val relativeCaptionDistance = 0.2F + val relativeValuesDistance = 0.1F + + // preprocessed values + val sliderWidth = w * relativeSliderWidth + val xoffset = (w - sliderWidth) * 0.5F + val captionsDistance = sliderDotRadius + h*relativeCaptionDistance + val valuesDistance = sliderDotRadius + h*relativeValuesDistance + val average = 0.5F + progress*( (avg-min)/(max-min) - 0.5F) + val averageX = sliderWidth * average +// val averageX = sliderWidth * (avg-min) / (max-min) + + // paints + val sliderPaint = Paint() + sliderPaint.style = Paint.Style.STROKE + sliderPaint.strokeCap = Paint.Cap.ROUND + sliderPaint.strokeWidth = sliderThickness + sliderPaint.color = Color.WHITE + + val dotPaint = Paint() + dotPaint.color = Color.WHITE + + val roboto = ResourcesCompat.getFont(ctx, R.font.roboto) + + val captionLeftPaint = Paint() + captionLeftPaint.typeface = roboto + captionLeftPaint.textAlign = Paint.Align.LEFT + captionLeftPaint.color = ResourcesCompat.getColor(ctx.resources, R.color.soft_grey, null) + captionLeftPaint.textSize = relativeCaptionSize*w + + val captionRightPaint = Paint() + captionRightPaint.typeface = roboto + captionRightPaint.textAlign = Paint.Align.RIGHT + captionRightPaint.color = ResourcesCompat.getColor(ctx.resources, R.color.soft_grey, null) + captionRightPaint.textSize = relativeCaptionSize*w + + val valuesLeftPaint = Paint() + valuesLeftPaint.typeface = roboto + valuesLeftPaint.textAlign = Paint.Align.LEFT + valuesLeftPaint.color = Color.WHITE + valuesLeftPaint.textSize = relativeValuesSize*w + + val valuesRightPaint = Paint() + valuesRightPaint.typeface = roboto + valuesRightPaint.textAlign = Paint.Align.RIGHT + valuesRightPaint.color = Color.WHITE + valuesRightPaint.textSize = relativeValuesSize*w + + // (1) draw slider + val slider = Path() + slider.moveTo(xoffset, h/2) + slider.lineTo(xoffset+sliderWidth, h/2) + canvas.drawPath(slider, sliderPaint) + + // (2) draw slider dots +// canvas.drawCircle(xoffset, h/2, sliderDotRadius, dotPaint) +// canvas.drawCircle(xoffset+sliderWidth, h/2, sliderDotRadius, dotPaint) + canvas.drawCircle(xoffset+averageX, h/2, sliderDotRadius, dotPaint) + + // (3) draw min+max captions + canvas.drawText("min", xoffset, h * 0.5F + captionsDistance, captionLeftPaint) + canvas.drawText("max", xoffset+sliderWidth, h/2 + captionsDistance, captionRightPaint) + + // (4) draw min+max values + canvas.drawText("${min}€", xoffset, h/2 - valuesDistance, valuesLeftPaint) + canvas.drawText("${max}€", xoffset+sliderWidth, h/2 - valuesDistance, valuesRightPaint) + + } + + + private fun drawAverage(){ + val relativeSampleSize = 0.03F + + // params + val relativeTriangleWidth = 0.04F + val relativeTriangleHeight = 0.2F + val relativeTooltipWidth = 0.2F + val relativeTooltipHeight = 0.7F + + // preprocessed values + val sliderWidth = w * relativeSliderWidth + val xoffset = (w - sliderWidth) * 0.5F + val yoffset = height - h +// val averageX = sliderWidth * (avg-min) / (max-min) + val average = 0.5F + progress*( (avg-min)/(max-min) - 0.5F) + val averageX = sliderWidth * average + + val triangleWidth = relativeTriangleWidth * w + val triangleHeight = relativeTriangleHeight * yoffset + + val tooltipWidth = relativeTooltipWidth * w + val tooltipHeight = relativeTooltipHeight * yoffset + + // (1) Draw triangle + val whitePaint = Paint() + whitePaint.color = Color.WHITE + whitePaint.flags = Paint.ANTI_ALIAS_FLAG + + val triangle = Path() + triangle.moveTo(xoffset+averageX, yoffset) + triangle.lineTo(xoffset+averageX-triangleWidth, yoffset + triangleHeight + 1F) // add 1 to overlap + triangle.lineTo(xoffset+averageX+triangleWidth, yoffset + triangleHeight + 1F) + canvas.drawPath(triangle, whitePaint) + + // (2) Draw tooltip background + canvas.drawRoundRect( + RectF( + xoffset+averageX - tooltipWidth/2, + yoffset + triangleHeight, + xoffset+averageX + tooltipWidth/2, + yoffset + triangleHeight + tooltipHeight), + 10F, 10F, + whitePaint) + + + // (3) Draw sample size + val sampleSizePaint = Paint() + sampleSizePaint.typeface = ResourcesCompat.getFont(ctx, R.font.roboto) + sampleSizePaint.textAlign = Paint.Align.CENTER + sampleSizePaint.color = ResourcesCompat.getColor(ctx.resources, R.color.soft_grey, null) + sampleSizePaint.textSize = relativeSampleSize*w + sampleSizePaint.flags = Paint.ANTI_ALIAS_FLAG + + canvas.drawText("sur ${size.toInt()} articles", xoffset+averageX, yoffset + triangleHeight + tooltipHeight + relativeSampleSize*w, sampleSizePaint) + + // (4) Draw average text + val roboto = ResourcesCompat.getFont(ctx, R.font.roboto) + + val averageTextPaint = Paint() + averageTextPaint.typeface = roboto + averageTextPaint.textAlign = Paint.Align.CENTER + averageTextPaint.color = ResourcesCompat.getColor(ctx.resources, R.color.colorAccent, null) + averageTextPaint.textSize = 50F + + canvas.drawText("${round((min+average*(max-min)) *10F)/10F}€", xoffset+averageX, yoffset+triangleHeight + tooltipHeight*0.6F, averageTextPaint) + + + + + } + + + +} \ No newline at end of file diff --git a/java/io/xdrm/lebonprix/anim/UnderlineAnimation.kt b/java/io/xdrm/lebonprix/anim/UnderlineAnimation.kt new file mode 100644 index 0000000..0b171e5 --- /dev/null +++ b/java/io/xdrm/lebonprix/anim/UnderlineAnimation.kt @@ -0,0 +1,81 @@ +package io.xdrm.lebonprix.anim + +import android.content.res.Resources +import android.graphics.* +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RectShape +import android.util.Log +import android.view.View +import android.view.animation.Animation +import android.view.animation.Transformation +import android.widget.ImageView + +class UnderlineAnimation(val target: ImageView?) : Animation() { + private val width : Int = 1000 + private var cursor : Float = 0F + private var start : Float = 0F + private var end : Float = 0F + + private var bitmap : Bitmap = Bitmap.createBitmap(width, 5, Bitmap.Config.ARGB_8888) + private var canvas : Canvas = Canvas(this.bitmap) + + init { + draw() + } + + fun during(durationMillis: Long) : UnderlineAnimation{ + super.setDuration(durationMillis) + return this + } + + // sets the animation range + fun animate(_start: Float, _end : Float) : UnderlineAnimation{ + // ignore animation if already at (or going for) the end state + if( _end == cursor || !hasEnded() && _end == end ) + return UnderlineAnimation(null) + + start = _start + end = _end + return this + } + + override fun start() { + if( target == null ) + return + + target.startAnimation(this) + } + + // sets the animation range + fun animateContinue(_end : Float) : UnderlineAnimation{ + start = cursor + end = _end + return this + } + + override fun applyTransformation(interpolatedTime: Float, t: Transformation?) { + super.applyTransformation(interpolatedTime, t) + cursor = start + (end - start) * interpolatedTime + draw() + } + + private fun draw(){ + // clear canvas + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + + // set up taint (WHITE) + val taint = Paint() + taint.color = Color.WHITE + + // draw rect + val size = cursor*width + val offset = (width-size)/2 + canvas.drawRect(0F, 0F, size, 5F, taint) + + // display on target + if( target != null ) + target.setImageBitmap(bitmap) + } + +} \ No newline at end of file diff --git a/java/io/xdrm/lebonprix/api/CategoryFetcher.kt b/java/io/xdrm/lebonprix/api/CategoryFetcher.kt new file mode 100644 index 0000000..199b022 --- /dev/null +++ b/java/io/xdrm/lebonprix/api/CategoryFetcher.kt @@ -0,0 +1,67 @@ +package io.xdrm.lebonprix.api + +import android.app.Activity +import android.app.AlertDialog +import android.app.Dialog +import android.content.Context +import android.content.DialogInterface +import android.graphics.Color +import android.support.v4.content.res.ResourcesCompat +import android.util.Log +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import io.xdrm.lebonprix.HomeActivity +import io.xdrm.lebonprix.R +import io.xdrm.lebonprix.anim.UnderlineAnimation +import io.xdrm.lebonprix.extensions.await +import kotlinx.android.synthetic.main.activity_home.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONArray +import java.net.URLEncoder + +class CategoryFetcher( + var act: Activity, + val anim_target: ImageView, + val anim: UnderlineAnimation, + val httpClient: OkHttpClient +) { + + + suspend fun fetch(keywords: String) : JSONArray { + if( keywords.isEmpty() ) + return JSONArray("[]") + + GlobalScope.launch(Dispatchers.Main){ anim.animate(0F,0.5F).during(500).start() } + + // 1. Prepare URL + val url = "https://www.lebonprix.info/api/categorizer?q=${ URLEncoder.encode(keywords, "UTF-8") }" + Log.i("API-Request", url) + + // 2. Get response + return try{ + + httpClient.newCall( Request.Builder().url(url).build() ) + .await() + .body() + ?.string() + .let{ raw -> JSONArray(raw) } + + }catch(e: Exception){ + if( e !is CancellationException) { + act.runOnUiThread { + Toast.makeText(act.applicationContext, "Erreur: impossible de récupérer les catégories", Toast.LENGTH_LONG).show() + } + } + + return JSONArray("[]") + } + + } + +} \ No newline at end of file diff --git a/java/io/xdrm/lebonprix/api/PricesFetcher.kt b/java/io/xdrm/lebonprix/api/PricesFetcher.kt new file mode 100644 index 0000000..60312b1 --- /dev/null +++ b/java/io/xdrm/lebonprix/api/PricesFetcher.kt @@ -0,0 +1,93 @@ +package io.xdrm.lebonprix.api + +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.widget.Toast +import io.xdrm.lebonprix.HomeActivity +import io.xdrm.lebonprix.extensions.await +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONObject +import java.net.URLEncoder + + +class PricesFetcher( + val act: Activity, + val httpClient: OkHttpClient +) { + + private val specialCharMap = mapOf( + Pair("&", " "), + Pair("é", "e"), + Pair("è", "e"), + Pair("ê", "e"), + Pair("î", "i"), + Pair("ô", "o"), + Pair("'", " "), + Pair("-", " ")) + + suspend fun fetch(query: String, categories: Array): Array { + val querySanitized = URLEncoder.encode(query, "utf-8") + + // query + if( categories.isEmpty() ) + return httpCall(querySanitized) + + val results = mutableListOf() + categories.forEach { + results.addAll(httpCall(querySanitized, it)) + } + + return results.toTypedArray() + } + + private fun sanitizeCategory(category: String) : String { + + return category.split("").map { + if (it in specialCharMap.keys) specialCharMap[it] else it + }.joinToString("") + + } + + suspend private fun httpCall(query: String, cat: String? = null): Array { + var url = "http://www.lebonprix.info/api/sampling?q=$query" + + if( !cat.isNullOrBlank() ) + url += "&c=${sanitizeCategory(cat)}" + + var error = false + + val result = try{ + + httpClient.newCall( Request.Builder().url(url).build() ) + .await() + .body() + ?.string() + .let{ JSONObject(it) } + .getJSONArray("sample") + .let{ arr -> Array( arr.length() ){ arr.getInt(it) } } + + }catch(e: Exception){ + if( e !is CancellationException) { + error = true + act.runOnUiThread { + Toast.makeText(act.applicationContext, "Erreur: impossible de récupérer les prix", Toast.LENGTH_LONG).show() + } + } + + return arrayOf() + } + + // nothing found + if( !error && result.isNullOrEmpty() ) + act.runOnUiThread { Toast.makeText(act.applicationContext, "Aucun résultat", Toast.LENGTH_LONG).show() } + + return result + } + +} \ No newline at end of file diff --git a/java/io/xdrm/lebonprix/extensions/Call.kt b/java/io/xdrm/lebonprix/extensions/Call.kt new file mode 100644 index 0000000..a81c2e1 --- /dev/null +++ b/java/io/xdrm/lebonprix/extensions/Call.kt @@ -0,0 +1,28 @@ +package io.xdrm.lebonprix.extensions + +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Response +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +suspend fun Call.await() : Response{ + return suspendCancellableCoroutine { + + it.invokeOnCancellation { + this.cancel() + } + + this.enqueue(object : Callback{ + override fun onFailure(call: Call, e: IOException) { + it.cancel(e) + } + + override fun onResponse(call: Call, response: Response) { + it.resume(response) + } + }) + } +} \ No newline at end of file diff --git a/java/io/xdrm/lebonprix/model/Category.kt b/java/io/xdrm/lebonprix/model/Category.kt new file mode 100644 index 0000000..2926153 --- /dev/null +++ b/java/io/xdrm/lebonprix/model/Category.kt @@ -0,0 +1,104 @@ +package io.xdrm.lebonprix.model + +import android.util.Log +import io.xdrm.lebonprix.R + +enum class Category(val label: String, val iconId: Int){ + + // [1] Common + ALL("Toutes les catégories", R.drawable.ic_category_all), + MISC("Autres", R.drawable.ic_category_misc), + + // [2] VEHICLES + CARS("Voitures", R.drawable.ic_category_cars), + MOTORCYCLES("Motos", R.drawable.ic_category_moto), + CARAVANS("Caravaning", R.drawable.ic_category_caravan), + UTILITY_VEHICLES("Utilitaires", R.drawable.ic_category_utility_vehicle), + CAR_EQUIPMENT("Equipement auto", R.drawable.ic_category_tools), + MOTORCYCLES_EQUIPMENT("Equipement moto", R.drawable.ic_category_tools), + CARAVAN_EQUIPMENT("Equipement caravaning", R.drawable.ic_category_tools), + YACHTING("Nautisme", R.drawable.ic_category_boat), + YACHTING_EQUIPMENT("Equipement nautisme", R.drawable.ic_category_tools), + + // [3] REAL ESTATE + HOUSE_SALES("Ventes immobilières", R.drawable.ic_category_house), + HOUSE("Immobilier neuf", R.drawable.ic_category_house), + LEASING("Locations", R.drawable.ic_category_apartment), + CO_LEASING("Colocations", R.drawable.ic_category_apartment), + OFFICES_SHOPS("Bureaux & Commerces", R.drawable.ic_category_market), + + // [4] Holidays + LEASING_GITES("Locations & Gîtes", R.drawable.ic_category_house), + GUESTHOUSE("Chambres d'hôtes", R.drawable.ic_category_house), + CAMPING("Campings", R.drawable.ic_category_camping), + HOTELS("Hotels", R.drawable.ic_category_hotel), + UNUSUAL_LEASING("Hébergements insolites", R.drawable.ic_category_camping), + + // [5] Multimedia + COMPUTER_EQUIPMENT("Informatique", R.drawable.ic_category_computer), + GAMES("Consoles & Jeux vidéo", R.drawable.ic_category_game), + VIDEO("Image & Son", R.drawable.ic_category_camera), + PHONES("Téléphonie", R.drawable.ic_category_phone), + + // [6] Entertainment + DVD("DVD / Films", R.drawable.ic_category_dvd), + CD("CD / Musique", R.drawable.ic_category_cd), + BOOKS("Livres", R.drawable.ic_category_book), + PETS("Animaux", R.drawable.ic_category_pet), + BIKES("Vélos", R.drawable.ic_category_bike), + HOBBIES("Sports & Hobbies", R.drawable.ic_category_sport), + MUSIC_INSTRUMENTS("Instruments de musique", R.drawable.ic_category_piano), + COLLECTION("Collection", R.drawable.ic_category_collection), + TOYS("Jeux & Jouets", R.drawable.ic_category_toy), + WINE("Vins & Gastronomie", R.drawable.ic_category_wine), + + // [7] Professional equipment + FARM_EQUIPMENT("Matériel agricole", R.drawable.ic_category_tractor), + WAREHOUSE_EQUIPMENT("Transport - Manutention", R.drawable.ic_category_warehouse), + CONSTRUCTION("BTP - Chantier gros-oeuvre", R.drawable.ic_category_road), + TOOLS("Outillage - Matériaux 2nd-oeuvre", R.drawable.ic_category_diy), + INDUSTRIAL_EQUIPMENT("Équipements industriels", R.drawable.ic_category_industry), + CATERING("Restauration - Hôtellerie", R.drawable.ic_category_food), + DESK_EQUIPMENT("Fournitures de bureau", R.drawable.ic_category_desk), + SHOP_EQUIPMENT("Commerces & Marchés", R.drawable.ic_category_market), + MEDICAL_EQUIPMENT("Matériel médical", R.drawable.ic_category_pill), + + // [8] Services + SERVICES("Prestations de services", R.drawable.ic_category_handshake), + TICKETS("Billetterie", R.drawable.ic_category_ticket), + EVENTS("Evénements", R.drawable.ic_category_event), + CLASSES("Cours particuliers", R.drawable.ic_category_teacher), + CAR_SHARING("Covoiturage", R.drawable.ic_category_car_sharing), + + // [9] House + FURNITURE("Ameublement", R.drawable.ic_category_furniture), + ELECTRICAL("Electroménager", R.drawable.ic_category_cooker), + TABLE_ARTS("Arts de la table", R.drawable.ic_category_food), + DECORATION("Décoration", R.drawable.ic_category_decoration), + LAUNDRY("Linge de maison", R.drawable.ic_category_pillow), + DIY("Bricolage", R.drawable.ic_category_diy), + GARDENING("Jardinage", R.drawable.ic_category_gardening), + + // [10] Clothes + CLOTHS("Vêtements", R.drawable.ic_category_cloth), + SHOES("Chaussures", R.drawable.ic_category_shoe), + LUGGAGE("Accessoires & Bagagerie", R.drawable.ic_category_bag), + JEWELS("Montres & Bijoux", R.drawable.ic_category_watch), + BABY_EQUIPMENT("Equipement bébé", R.drawable.ic_category_baby), + BABY_CLOTHS("Vêtements bébé", R.drawable.ic_category_baby); + + + companion object { + fun fromLabel(l: String): Category? { + val lower = l.toLowerCase() + val avail = Category.values() + + for (cat in avail) { + if (cat.label.toLowerCase() == lower) + return cat + } + Log.i("DEBUGDEBUG", "unknown label " + lower) + return null + } + } +} \ No newline at end of file diff --git a/java/io/xdrm/lebonprix/model/CategoryItem.kt b/java/io/xdrm/lebonprix/model/CategoryItem.kt new file mode 100644 index 0000000..80ced75 --- /dev/null +++ b/java/io/xdrm/lebonprix/model/CategoryItem.kt @@ -0,0 +1,11 @@ +package io.xdrm.lebonprix.model + +data class CategoryItem ( + var category : Category, + var selected : Boolean, + var count : Float +) + +object CategoryItemStore { + val data = mutableListOf() +} \ No newline at end of file diff --git a/res/drawable-v24/ic_launcher_foreground.xml b/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/res/drawable/ic_category_all.xml b/res/drawable/ic_category_all.xml new file mode 100644 index 0000000..06dfac9 --- /dev/null +++ b/res/drawable/ic_category_all.xml @@ -0,0 +1,7 @@ + + + diff --git a/res/drawable/ic_category_apartment.xml b/res/drawable/ic_category_apartment.xml new file mode 100644 index 0000000..38217a1 --- /dev/null +++ b/res/drawable/ic_category_apartment.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_baby.xml b/res/drawable/ic_category_baby.xml new file mode 100644 index 0000000..1965887 --- /dev/null +++ b/res/drawable/ic_category_baby.xml @@ -0,0 +1,8 @@ + + + + diff --git a/res/drawable/ic_category_bag.xml b/res/drawable/ic_category_bag.xml new file mode 100644 index 0000000..e9db200 --- /dev/null +++ b/res/drawable/ic_category_bag.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_bike.xml b/res/drawable/ic_category_bike.xml new file mode 100644 index 0000000..24382ae --- /dev/null +++ b/res/drawable/ic_category_bike.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_boat.xml b/res/drawable/ic_category_boat.xml new file mode 100644 index 0000000..009d007 --- /dev/null +++ b/res/drawable/ic_category_boat.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_book.xml b/res/drawable/ic_category_book.xml new file mode 100644 index 0000000..a178cfb --- /dev/null +++ b/res/drawable/ic_category_book.xml @@ -0,0 +1,6 @@ + + + diff --git a/res/drawable/ic_category_camera.xml b/res/drawable/ic_category_camera.xml new file mode 100644 index 0000000..ee5f4e4 --- /dev/null +++ b/res/drawable/ic_category_camera.xml @@ -0,0 +1,6 @@ + + + diff --git a/res/drawable/ic_category_camping.xml b/res/drawable/ic_category_camping.xml new file mode 100644 index 0000000..ce5f1cf --- /dev/null +++ b/res/drawable/ic_category_camping.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_car_sharing.xml b/res/drawable/ic_category_car_sharing.xml new file mode 100644 index 0000000..7f5ba64 --- /dev/null +++ b/res/drawable/ic_category_car_sharing.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_caravan.xml b/res/drawable/ic_category_caravan.xml new file mode 100644 index 0000000..3742cc2 --- /dev/null +++ b/res/drawable/ic_category_caravan.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_cars.xml b/res/drawable/ic_category_cars.xml new file mode 100644 index 0000000..f779d8c --- /dev/null +++ b/res/drawable/ic_category_cars.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_cd.xml b/res/drawable/ic_category_cd.xml new file mode 100644 index 0000000..93f1718 --- /dev/null +++ b/res/drawable/ic_category_cd.xml @@ -0,0 +1,7 @@ + + + diff --git a/res/drawable/ic_category_cloth.xml b/res/drawable/ic_category_cloth.xml new file mode 100644 index 0000000..06d7f83 --- /dev/null +++ b/res/drawable/ic_category_cloth.xml @@ -0,0 +1,6 @@ + + + diff --git a/res/drawable/ic_category_collection.xml b/res/drawable/ic_category_collection.xml new file mode 100644 index 0000000..d01fe65 --- /dev/null +++ b/res/drawable/ic_category_collection.xml @@ -0,0 +1,7 @@ + + + diff --git a/res/drawable/ic_category_computer.xml b/res/drawable/ic_category_computer.xml new file mode 100644 index 0000000..cdbf29d --- /dev/null +++ b/res/drawable/ic_category_computer.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_cooker.xml b/res/drawable/ic_category_cooker.xml new file mode 100644 index 0000000..02cd976 --- /dev/null +++ b/res/drawable/ic_category_cooker.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_decoration.xml b/res/drawable/ic_category_decoration.xml new file mode 100644 index 0000000..a43622e --- /dev/null +++ b/res/drawable/ic_category_decoration.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_desk.xml b/res/drawable/ic_category_desk.xml new file mode 100644 index 0000000..f2ba52f --- /dev/null +++ b/res/drawable/ic_category_desk.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_diy.xml b/res/drawable/ic_category_diy.xml new file mode 100644 index 0000000..b42bc19 --- /dev/null +++ b/res/drawable/ic_category_diy.xml @@ -0,0 +1,8 @@ + + + diff --git a/res/drawable/ic_category_dvd.xml b/res/drawable/ic_category_dvd.xml new file mode 100644 index 0000000..8d31edd --- /dev/null +++ b/res/drawable/ic_category_dvd.xml @@ -0,0 +1,7 @@ + + + diff --git a/res/drawable/ic_category_event.xml b/res/drawable/ic_category_event.xml new file mode 100644 index 0000000..0e31be3 --- /dev/null +++ b/res/drawable/ic_category_event.xml @@ -0,0 +1,7 @@ + + + diff --git a/res/drawable/ic_category_food.xml b/res/drawable/ic_category_food.xml new file mode 100644 index 0000000..764cd1f --- /dev/null +++ b/res/drawable/ic_category_food.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_furniture.xml b/res/drawable/ic_category_furniture.xml new file mode 100644 index 0000000..15c04d1 --- /dev/null +++ b/res/drawable/ic_category_furniture.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_game.xml b/res/drawable/ic_category_game.xml new file mode 100644 index 0000000..b3922cd --- /dev/null +++ b/res/drawable/ic_category_game.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_gardening.xml b/res/drawable/ic_category_gardening.xml new file mode 100644 index 0000000..c0adb2a --- /dev/null +++ b/res/drawable/ic_category_gardening.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_handshake.xml b/res/drawable/ic_category_handshake.xml new file mode 100644 index 0000000..d22b183 --- /dev/null +++ b/res/drawable/ic_category_handshake.xml @@ -0,0 +1,6 @@ + + + diff --git a/res/drawable/ic_category_hotel.xml b/res/drawable/ic_category_hotel.xml new file mode 100644 index 0000000..17491af --- /dev/null +++ b/res/drawable/ic_category_hotel.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_house.xml b/res/drawable/ic_category_house.xml new file mode 100644 index 0000000..f7699e2 --- /dev/null +++ b/res/drawable/ic_category_house.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_industry.xml b/res/drawable/ic_category_industry.xml new file mode 100644 index 0000000..5bc95fb --- /dev/null +++ b/res/drawable/ic_category_industry.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_market.xml b/res/drawable/ic_category_market.xml new file mode 100644 index 0000000..8058bab --- /dev/null +++ b/res/drawable/ic_category_market.xml @@ -0,0 +1,4 @@ + + + diff --git a/res/drawable/ic_category_misc.xml b/res/drawable/ic_category_misc.xml new file mode 100644 index 0000000..53c1ee3 --- /dev/null +++ b/res/drawable/ic_category_misc.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/res/drawable/ic_category_moto.xml b/res/drawable/ic_category_moto.xml new file mode 100644 index 0000000..5d3170e --- /dev/null +++ b/res/drawable/ic_category_moto.xml @@ -0,0 +1,8 @@ + + + diff --git a/res/drawable/ic_category_pet.xml b/res/drawable/ic_category_pet.xml new file mode 100644 index 0000000..8d25c2d --- /dev/null +++ b/res/drawable/ic_category_pet.xml @@ -0,0 +1,11 @@ + + + diff --git a/res/drawable/ic_category_phone.xml b/res/drawable/ic_category_phone.xml new file mode 100644 index 0000000..87f3220 --- /dev/null +++ b/res/drawable/ic_category_phone.xml @@ -0,0 +1,4 @@ + + + diff --git a/res/drawable/ic_category_piano.xml b/res/drawable/ic_category_piano.xml new file mode 100644 index 0000000..51d7e59 --- /dev/null +++ b/res/drawable/ic_category_piano.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_pill.xml b/res/drawable/ic_category_pill.xml new file mode 100644 index 0000000..2297aa2 --- /dev/null +++ b/res/drawable/ic_category_pill.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_pillow.xml b/res/drawable/ic_category_pillow.xml new file mode 100644 index 0000000..ace1220 --- /dev/null +++ b/res/drawable/ic_category_pillow.xml @@ -0,0 +1,8 @@ + + + diff --git a/res/drawable/ic_category_road.xml b/res/drawable/ic_category_road.xml new file mode 100644 index 0000000..0db1bdb --- /dev/null +++ b/res/drawable/ic_category_road.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_shoe.xml b/res/drawable/ic_category_shoe.xml new file mode 100644 index 0000000..76483e3 --- /dev/null +++ b/res/drawable/ic_category_shoe.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_sport.xml b/res/drawable/ic_category_sport.xml new file mode 100644 index 0000000..92d8ed6 --- /dev/null +++ b/res/drawable/ic_category_sport.xml @@ -0,0 +1,8 @@ + + + diff --git a/res/drawable/ic_category_teacher.xml b/res/drawable/ic_category_teacher.xml new file mode 100644 index 0000000..d71d6cc --- /dev/null +++ b/res/drawable/ic_category_teacher.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_ticket.xml b/res/drawable/ic_category_ticket.xml new file mode 100644 index 0000000..4e896e9 --- /dev/null +++ b/res/drawable/ic_category_ticket.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_tools.xml b/res/drawable/ic_category_tools.xml new file mode 100644 index 0000000..5b36afe --- /dev/null +++ b/res/drawable/ic_category_tools.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_toy.xml b/res/drawable/ic_category_toy.xml new file mode 100644 index 0000000..f786e3d --- /dev/null +++ b/res/drawable/ic_category_toy.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_tractor.xml b/res/drawable/ic_category_tractor.xml new file mode 100644 index 0000000..1be1eb8 --- /dev/null +++ b/res/drawable/ic_category_tractor.xml @@ -0,0 +1,8 @@ + + + diff --git a/res/drawable/ic_category_utility_vehicle.xml b/res/drawable/ic_category_utility_vehicle.xml new file mode 100644 index 0000000..fd5a435 --- /dev/null +++ b/res/drawable/ic_category_utility_vehicle.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_warehouse.xml b/res/drawable/ic_category_warehouse.xml new file mode 100644 index 0000000..01aca88 --- /dev/null +++ b/res/drawable/ic_category_warehouse.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_watch.xml b/res/drawable/ic_category_watch.xml new file mode 100644 index 0000000..1577639 --- /dev/null +++ b/res/drawable/ic_category_watch.xml @@ -0,0 +1,5 @@ + + + diff --git a/res/drawable/ic_category_wine.xml b/res/drawable/ic_category_wine.xml new file mode 100644 index 0000000..04c74be --- /dev/null +++ b/res/drawable/ic_category_wine.xml @@ -0,0 +1,4 @@ + + + diff --git a/res/drawable/ic_home_bg.xml b/res/drawable/ic_home_bg.xml new file mode 100644 index 0000000..65807f9 --- /dev/null +++ b/res/drawable/ic_home_bg.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/res/drawable/ic_icon.xml b/res/drawable/ic_icon.xml new file mode 100644 index 0000000..c541397 --- /dev/null +++ b/res/drawable/ic_icon.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/res/drawable/ic_icon_white.xml b/res/drawable/ic_icon_white.xml new file mode 100644 index 0000000..c39e1c5 --- /dev/null +++ b/res/drawable/ic_icon_white.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/res/drawable/ic_launcher_background.xml b/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..a0ad202 --- /dev/null +++ b/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/drawable/ic_results_bg.xml b/res/drawable/ic_results_bg.xml new file mode 100644 index 0000000..6cfd2ff --- /dev/null +++ b/res/drawable/ic_results_bg.xml @@ -0,0 +1,6 @@ + + + diff --git a/res/drawable/ic_search.xml b/res/drawable/ic_search.xml new file mode 100644 index 0000000..4a21a04 --- /dev/null +++ b/res/drawable/ic_search.xml @@ -0,0 +1,7 @@ + + + diff --git a/res/drawable/ic_splash_icon.xml b/res/drawable/ic_splash_icon.xml new file mode 100644 index 0000000..bdc63c8 --- /dev/null +++ b/res/drawable/ic_splash_icon.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/res/drawable/rounded_category_bg.xml b/res/drawable/rounded_category_bg.xml new file mode 100644 index 0000000..025dda2 --- /dev/null +++ b/res/drawable/rounded_category_bg.xml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/rounded_category_item_bg.xml b/res/drawable/rounded_category_item_bg.xml new file mode 100644 index 0000000..9ffcb77 --- /dev/null +++ b/res/drawable/rounded_category_item_bg.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/rounded_category_item_bg_selected.xml b/res/drawable/rounded_category_item_bg_selected.xml new file mode 100644 index 0000000..610fc6a --- /dev/null +++ b/res/drawable/rounded_category_item_bg_selected.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/rounded_category_item_count_bg.xml b/res/drawable/rounded_category_item_count_bg.xml new file mode 100644 index 0000000..610fc6a --- /dev/null +++ b/res/drawable/rounded_category_item_count_bg.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/rounded_progress_bg.xml b/res/drawable/rounded_progress_bg.xml new file mode 100644 index 0000000..00263d9 --- /dev/null +++ b/res/drawable/rounded_progress_bg.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/rounded_progress_item.xml b/res/drawable/rounded_progress_item.xml new file mode 100644 index 0000000..8106745 --- /dev/null +++ b/res/drawable/rounded_progress_item.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/res/font/roboto.ttf b/res/font/roboto.ttf new file mode 100644 index 0000000..664e1b2 Binary files /dev/null and b/res/font/roboto.ttf differ diff --git a/res/layout/activity_gooey_splash_screen.xml b/res/layout/activity_gooey_splash_screen.xml new file mode 100644 index 0000000..715b2f3 --- /dev/null +++ b/res/layout/activity_gooey_splash_screen.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/res/layout/activity_home.xml b/res/layout/activity_home.xml new file mode 100644 index 0000000..8fbb830 --- /dev/null +++ b/res/layout/activity_home.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/activity_result.xml b/res/layout/activity_result.xml new file mode 100644 index 0000000..2c3ece2 --- /dev/null +++ b/res/layout/activity_result.xml @@ -0,0 +1,36 @@ + + + + + + + \ No newline at end of file diff --git a/res/layout/category_item.xml b/res/layout/category_item.xml new file mode 100644 index 0000000..b710025 --- /dev/null +++ b/res/layout/category_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/mipmap-anydpi-v26/ic_launcher_default.xml b/res/mipmap-anydpi-v26/ic_launcher_default.xml new file mode 100644 index 0000000..9412204 --- /dev/null +++ b/res/mipmap-anydpi-v26/ic_launcher_default.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/mipmap-anydpi-v26/ic_launcher_default_round.xml b/res/mipmap-anydpi-v26/ic_launcher_default_round.xml new file mode 100644 index 0000000..9412204 --- /dev/null +++ b/res/mipmap-anydpi-v26/ic_launcher_default_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/res/mipmap-hdpi/ic_launcher.png b/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..01b60f1 Binary files /dev/null and b/res/mipmap-hdpi/ic_launcher.png differ diff --git a/res/mipmap-hdpi/ic_launcher_default.png b/res/mipmap-hdpi/ic_launcher_default.png new file mode 100644 index 0000000..567037d Binary files /dev/null and b/res/mipmap-hdpi/ic_launcher_default.png differ diff --git a/res/mipmap-hdpi/ic_launcher_default_round.png b/res/mipmap-hdpi/ic_launcher_default_round.png new file mode 100644 index 0000000..b5ae90a Binary files /dev/null and b/res/mipmap-hdpi/ic_launcher_default_round.png differ diff --git a/res/mipmap-hdpi/ic_launcher_foreground.png b/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..01b60f1 Binary files /dev/null and b/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/res/mipmap-hdpi/ic_splash_icon.png b/res/mipmap-hdpi/ic_splash_icon.png new file mode 100644 index 0000000..9f6c9cc Binary files /dev/null and b/res/mipmap-hdpi/ic_splash_icon.png differ diff --git a/res/mipmap-mdpi/ic_launcher.png b/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..9395351 Binary files /dev/null and b/res/mipmap-mdpi/ic_launcher.png differ diff --git a/res/mipmap-mdpi/ic_launcher_default.png b/res/mipmap-mdpi/ic_launcher_default.png new file mode 100644 index 0000000..79b5fb6 Binary files /dev/null and b/res/mipmap-mdpi/ic_launcher_default.png differ diff --git a/res/mipmap-mdpi/ic_launcher_default_round.png b/res/mipmap-mdpi/ic_launcher_default_round.png new file mode 100644 index 0000000..b59b44e Binary files /dev/null and b/res/mipmap-mdpi/ic_launcher_default_round.png differ diff --git a/res/mipmap-mdpi/ic_launcher_foreground.png b/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..9395351 Binary files /dev/null and b/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/res/mipmap-mdpi/ic_splash_icon.png b/res/mipmap-mdpi/ic_splash_icon.png new file mode 100644 index 0000000..e93ef53 Binary files /dev/null and b/res/mipmap-mdpi/ic_splash_icon.png differ diff --git a/res/mipmap-xhdpi/ic_launcher.png b/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..798d814 Binary files /dev/null and b/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/res/mipmap-xhdpi/ic_launcher_default.png b/res/mipmap-xhdpi/ic_launcher_default.png new file mode 100644 index 0000000..037ac87 Binary files /dev/null and b/res/mipmap-xhdpi/ic_launcher_default.png differ diff --git a/res/mipmap-xhdpi/ic_launcher_default_round.png b/res/mipmap-xhdpi/ic_launcher_default_round.png new file mode 100644 index 0000000..7006ac5 Binary files /dev/null and b/res/mipmap-xhdpi/ic_launcher_default_round.png differ diff --git a/res/mipmap-xhdpi/ic_launcher_foreground.png b/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..798d814 Binary files /dev/null and b/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/res/mipmap-xhdpi/ic_splash_icon.png b/res/mipmap-xhdpi/ic_splash_icon.png new file mode 100644 index 0000000..3ec13e0 Binary files /dev/null and b/res/mipmap-xhdpi/ic_splash_icon.png differ diff --git a/res/mipmap-xxhdpi/ic_launcher.png b/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..618e9fe Binary files /dev/null and b/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/res/mipmap-xxhdpi/ic_launcher_default.png b/res/mipmap-xxhdpi/ic_launcher_default.png new file mode 100644 index 0000000..185d1f6 Binary files /dev/null and b/res/mipmap-xxhdpi/ic_launcher_default.png differ diff --git a/res/mipmap-xxhdpi/ic_launcher_default_round.png b/res/mipmap-xxhdpi/ic_launcher_default_round.png new file mode 100644 index 0000000..b3aacac Binary files /dev/null and b/res/mipmap-xxhdpi/ic_launcher_default_round.png differ diff --git a/res/mipmap-xxhdpi/ic_launcher_foreground.png b/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..618e9fe Binary files /dev/null and b/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/res/mipmap-xxhdpi/ic_splash_icon.png b/res/mipmap-xxhdpi/ic_splash_icon.png new file mode 100644 index 0000000..64bf4a9 Binary files /dev/null and b/res/mipmap-xxhdpi/ic_splash_icon.png differ diff --git a/res/mipmap-xxxhdpi/ic_launcher.png b/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..452d7c3 Binary files /dev/null and b/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/res/mipmap-xxxhdpi/ic_launcher_default.png b/res/mipmap-xxxhdpi/ic_launcher_default.png new file mode 100644 index 0000000..fe0fb78 Binary files /dev/null and b/res/mipmap-xxxhdpi/ic_launcher_default.png differ diff --git a/res/mipmap-xxxhdpi/ic_launcher_default_round.png b/res/mipmap-xxxhdpi/ic_launcher_default_round.png new file mode 100644 index 0000000..b43e482 Binary files /dev/null and b/res/mipmap-xxxhdpi/ic_launcher_default_round.png differ diff --git a/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..452d7c3 Binary files /dev/null and b/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/res/mipmap-xxxhdpi/ic_splash_icon.png b/res/mipmap-xxxhdpi/ic_splash_icon.png new file mode 100644 index 0000000..4b93f2f Binary files /dev/null and b/res/mipmap-xxxhdpi/ic_splash_icon.png differ diff --git a/res/raw/drop.wav b/res/raw/drop.wav new file mode 100644 index 0000000..b957309 Binary files /dev/null and b/res/raw/drop.wav differ diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml new file mode 100644 index 0000000..78905b1 --- /dev/null +++ b/res/values-land/dimens.xml @@ -0,0 +1,4 @@ + + + 0dp + \ No newline at end of file diff --git a/res/values-land/integers.xml b/res/values-land/integers.xml new file mode 100644 index 0000000..46c9f94 --- /dev/null +++ b/res/values-land/integers.xml @@ -0,0 +1,4 @@ + + + 5 + \ No newline at end of file diff --git a/res/values-port/dimens.xml b/res/values-port/dimens.xml new file mode 100644 index 0000000..9e18389 --- /dev/null +++ b/res/values-port/dimens.xml @@ -0,0 +1,4 @@ + + + 110dp + \ No newline at end of file diff --git a/res/values/colors.xml b/res/values/colors.xml new file mode 100644 index 0000000..0b73fe3 --- /dev/null +++ b/res/values/colors.xml @@ -0,0 +1,9 @@ + + + #f64040 + #00574B + #05d6a0 + #3f64e9 + #d0d0d0 + #e6e6e6 + diff --git a/res/values/dimens.xml b/res/values/dimens.xml new file mode 100644 index 0000000..504086b --- /dev/null +++ b/res/values/dimens.xml @@ -0,0 +1,4 @@ + + + 80dp + \ No newline at end of file diff --git a/res/values/ic_launcher_background.xml b/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/res/values/integers.xml b/res/values/integers.xml new file mode 100644 index 0000000..db9b1ab --- /dev/null +++ b/res/values/integers.xml @@ -0,0 +1,4 @@ + + + 3 + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml new file mode 100644 index 0000000..2deadd7 --- /dev/null +++ b/res/values/strings.xml @@ -0,0 +1,5 @@ + + lebonprix + Chargement en cours, veuillez patienter + Chercher un produit leboncoin.fr + \ No newline at end of file diff --git a/res/values/styles.xml b/res/values/styles.xml new file mode 100644 index 0000000..99a51fb --- /dev/null +++ b/res/values/styles.xml @@ -0,0 +1,13 @@ + + + + + +