From 5a8cfc055dcdddbd2ba342e7a5bb78802cee395e Mon Sep 17 00:00:00 2001 From: ange Date: Thu, 16 Jan 2025 12:27:59 +0000 Subject: [PATCH 1/7] CI/CD auto deploy website (#4) Co-authored-by: stcb <21@stcb.cc> Reviewed-on: https://git.gmoker.com/icing/G-EIP-700-TLS-7-1-eip-stephane.corbiere/pulls/4 Co-authored-by: ange Co-committed-by: ange --- website/Dockerfile | 21 +-- website/compose.yaml | 16 +++ website/css/about.css | 171 ++++++++++++++++++++++++ website/css/index.css | 16 +++ website/css/style.css | 4 + website/go.mod | 3 + website/go.sum | 0 website/html/about.html | 63 +++++++++ website/html/description.html | 229 -------------------------------- website/html/index.html | 30 +---- website/html/style.css | 11 -- website/main.go | 32 ----- website/manifests/bin/deploy.sh | 27 ++-- website/manifests/bin/devel.sh | 1 + website/manifests/bin/prod.sh | 1 + website/open.sh | 5 + website/src/main.go | 14 ++ website/src/route.go | 51 +++++++ website/src/tmpl.go | 31 +++++ website/src/views.go | 29 ++++ website/tmpl/head.tmpl | 3 + website/tmpl/vars.tmpl | 19 +++ 22 files changed, 459 insertions(+), 318 deletions(-) create mode 100644 website/css/about.css create mode 100644 website/css/index.css create mode 100644 website/css/style.css create mode 100644 website/go.mod create mode 100644 website/go.sum create mode 100644 website/html/about.html delete mode 100644 website/html/description.html delete mode 100644 website/html/style.css delete mode 100644 website/main.go create mode 100755 website/open.sh create mode 100644 website/src/main.go create mode 100644 website/src/route.go create mode 100644 website/src/tmpl.go create mode 100644 website/src/views.go create mode 100644 website/tmpl/head.tmpl create mode 100644 website/tmpl/vars.tmpl diff --git a/website/Dockerfile b/website/Dockerfile index 7fda515..22f3387 100644 --- a/website/Dockerfile +++ b/website/Dockerfile @@ -1,14 +1,15 @@ -ARG VER=1.23 - -FROM "docker.io/golang:$VER" as build +FROM docker.io/golang:1.23 AS build WORKDIR /build/ -ARG VER -COPY main.go . -RUN printf "module main\ngo $VER" > go.mod && CGO_ENABLED=0 go build -o /app +COPY go.mod go.sum . +RUN go mod download +COPY src/ . +RUN CGO_ENABLED=0 go build -o /app FROM scratch -COPY --from=build /app /app -COPY static/ /static/ -COPY html/ /html/ +COPY --from=build /app . +COPY html/ html/ +COPY css/ css/ +COPY static/ static/ +COPY tmpl/ tmpl/ EXPOSE 3000 -CMD ["/app"] +CMD ["./app"] diff --git a/website/compose.yaml b/website/compose.yaml index 89ab5a7..2660b08 100644 --- a/website/compose.yaml +++ b/website/compose.yaml @@ -4,3 +4,19 @@ services: build: . ports: - "3000:3000" + develop: + watch: + - action: rebuild + path: src/ + - action: sync+restart + path: tmpl/ + target: tmpl/ + - action: sync+restart + path: html/ + target: html/ + - action: sync + path: css/ + target: css/ + - action: sync + path: static/ + target: static/ diff --git a/website/css/about.css b/website/css/about.css new file mode 100644 index 0000000..9283733 --- /dev/null +++ b/website/css/about.css @@ -0,0 +1,171 @@ +:root { + --primary-color: #000000; + --background-color: #f5f5f5; + --text-color: #333; + --secondary-text-color: #777; +} + +.content { + margin: 20px auto; + max-width: 900px; + padding: 40px; + background-color: var(--background-color); + color: var(--text-color); + border-radius: 8px; + font-family: 'Open Sans', Arial, sans-serif; + position: relative; + overflow: hidden; +} + +.title { + font-size: 2.5em; + color: var(--primary-color); + margin-bottom: 30px; + text-align: center; + animation: fadeInDown 1s ease; +} + +.section-title { + font-size: 1.8em; + color: var(--primary-color); + margin-top: 40px; + margin-bottom: 20px; + position: relative; + animation: fadeInLeft 1s ease; +} + +.section-title::after { + content: ''; + width: 50px; + height: 3px; + background-color: var(--primary-color); + position: absolute; + bottom: -10px; + left: 0; +} + +p, li { + line-height: 1.6; + font-size: 1.1em; + animation: fadeIn 1s ease; +} + +ul { + margin-left: 20px; + list-style-type: disc; +} + +.features ul li { + margin-bottom: 10px; +} + +.team-list { + list-style-type: none; + padding: 0; +} + +.team-list li { + margin-bottom: 8px; + font-weight: bold; + color: var(--text-color); +} + +.back-link-container { + text-align: center; + margin-top: 40px; + animation: fadeInUp 1s ease; +} + +.back-link { + text-decoration: none; + color: var(--primary-color); + font-weight: bold; + border: 2px solid var(--primary-color); + padding: 10px 20px; + border-radius: 5px; + transition: background-color 0.3s, color 0.3s; +} + +.back-link:hover { + background-color: var(--primary-color); + color: #fff; +} + +@keyframes fadeInDown { + from { + opacity: 0; + transform: translateY(-20px); + } to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeInLeft { + from { + opacity: 0; + transform: translateX(-20px); + } to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } to { + opacity: 1; + } +} + +@media (max-width: 768px) { + .content { + padding: 20px; + } + + .title { + font-size: 2em; + } + + .section-title { + font-size: 1.5em; + } + + p, li { + font-size: 1em; + } +} + +@media (max-width: 480px) { + .content { + padding: 15px; + } + + .title { + font-size: 1.8em; + } + + .section-title { + font-size: 1.2em; + } + + p, li { + font-size: 0.9em; + } +} + +a { + color: var(--primary-color); + text-decoration: none; +} diff --git a/website/css/index.css b/website/css/index.css new file mode 100644 index 0000000..de47602 --- /dev/null +++ b/website/css/index.css @@ -0,0 +1,16 @@ +body { + margin: 0; +} + +div { + position: absolute; + height: 100%; + width: 100%; +} + +.title { + display: flex; + align-items: center; + justify-content: center; + font-size: 3em; +} diff --git a/website/css/style.css b/website/css/style.css new file mode 100644 index 0000000..bda2311 --- /dev/null +++ b/website/css/style.css @@ -0,0 +1,4 @@ +a.nostyle { + color: inherit; + cursor: pointer; +} diff --git a/website/go.mod b/website/go.mod new file mode 100644 index 0000000..3a3a5a6 --- /dev/null +++ b/website/go.mod @@ -0,0 +1,3 @@ +module main + +go 1.23.3 diff --git a/website/go.sum b/website/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/website/html/about.html b/website/html/about.html new file mode 100644 index 0000000..12529e0 --- /dev/null +++ b/website/html/about.html @@ -0,0 +1,63 @@ + + + + {{template "head.tmpl". }} + + +
+

What is Icing?

+ +
+

+ Icing is a simple, lightweight, and efficient dialer designed to + replace your everyday phone app. It ensures end-to-end encryption of + telephone communications by implementing a home-made, analogic-based + voice encryption. Inspired by SRTP (Secure Real-time Transport + Protocol), using ECDH (Elliptic Curve Diffie-Hellman). +

+
+ +
+

Key Features

+
    +
  • + End-to-End Encryption: Secure your calls with + robust encryption protocols. +
  • +
  • + Transparent: If your peer doesn't use Icing, the + call remains completely normal. +
  • +
  • + Analogic-based: An open-source, exportable, + protocol that works without internet. +
  • +
+
+ +
+

How It Works

+

+ Icing generates a cryptographic key pair for you. Share your public key + with a neat QR code. +

+

+ During a call between two Icing users, voices are encrypted, + compressed, and transmitted via the telephone network using the Icing + Acoustic Protocol. +

+
+ +
+

The Team

+
    +
  • {{template "stephane"}}
  • +
  • {{template "alexis"}}
  • +
  • {{template "ange"}}
  • +
  • {{template "bartosz"}}
  • +
  • {{template "florian"}}
  • +
+
+
+ + diff --git a/website/html/description.html b/website/html/description.html deleted file mode 100644 index 296db36..0000000 --- a/website/html/description.html +++ /dev/null @@ -1,229 +0,0 @@ - - - - - Icing - - - - -
-

Project Description

- -
-

What is Icing?

-

- Icing is a simple, lightweight, and efficient dialer designed to replace your everyday phone app. It ensures end-to-end encryption of telephone communications by implementing a home-made, analogic-based voice encryption. Inspired by SRTP (Secure Real-time Transport Protocol), using ECDH (Elliptic Curve Diffie-Hellman). -

-
- -
-

Key Features

-
    -
  • End-to-End Encryption: Secure your calls with robust encryption protocols.
  • -
  • Transparent: If your peer doesn't use Icing, the call remains completely normal.
  • -
  • Analogic-based: An open-source, exportable, protocol that works without internet.
  • -
-
- -
-

How It Works

-

- Icing generates a cryptographic key pair for you. Share your public key with a neat QR code. -

-

- During a call between two Icing users, voices are encrypted, compressed, and transmitted via the telephone network using the Icing Acoustic Protocol. -

-
- -
-

Our Team

-

- We are a team of five dedicated individuals working on this solution: -

- -
-
- - diff --git a/website/html/index.html b/website/html/index.html index e1ec852..7e79f0a 100644 --- a/website/html/index.html +++ b/website/html/index.html @@ -1,31 +1,13 @@ - + {{template "head.tmpl" .}} -
-

- ICING -

-
+ +
+

ICING

+
+
diff --git a/website/html/style.css b/website/html/style.css deleted file mode 100644 index acdc431..0000000 --- a/website/html/style.css +++ /dev/null @@ -1,11 +0,0 @@ -/*! -Pure v3.0.0 -Copyright 2013 Yahoo! -Licensed under the BSD License. -https://github.com/pure-css/pure/blob/master/LICENSE -*/ -/*! -normalize.css v | MIT License | https://necolas.github.io/normalize.css/ -Copyright (c) Nicolas Gallagher and Jonathan Neal -*/ -/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u{display:inline-block;vertical-align:top}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-0.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent;cursor:default}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} \ No newline at end of file diff --git a/website/main.go b/website/main.go deleted file mode 100644 index d6a2066..0000000 --- a/website/main.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "log" - "net/http" - "path/filepath" -) - -func route(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/style.css" { - http.ServeFile(w, r, "/html/style.css") - return - } - if len(r.URL.Path) > len("/static/") && r.URL.Path[:len("/static/")] == "/static/" { - http.ServeFile(w, r, r.URL.Path) - return - } - if r.URL.Path == "/" { - http.ServeFile(w, r, "/html/index.html") - return - } - http.ServeFile(w, r, filepath.Join("/html", r.URL.Path + ".html")) -} - -func main() { - http.HandleFunc("/", route) - - err := http.ListenAndServe(":3000", nil) - if err != nil { - log.Fatal(err) - } -} diff --git a/website/manifests/bin/deploy.sh b/website/manifests/bin/deploy.sh index 088dfbb..2cc0c9e 100755 --- a/website/manifests/bin/deploy.sh +++ b/website/manifests/bin/deploy.sh @@ -3,32 +3,35 @@ set -o pipefail function kapply() { for f in "$@"; do - kubectl apply -f \ - <(envsubst "$(env | xargs printf '$%s ')" < "manifests/$f") + kubectl apply -f <(envsubst < "manifests/$f") done -} +}; export -f kapply function kcreatesec() { - kubectl create secret generic --save-config --dry-run=client -oyaml "$@" | kubectl apply -f- -} + kubectl create secret generic --dry-run=client -oyaml "$@" | kubectl replace -f- +}; export -f kcreatesec function kcreatecm() { - kubectl create configmap --dry-run=client -oyaml "$@" | kubectl apply -f- -} + kubectl create configmap --dry-run=client -oyaml "$@" | kubectl replace -f- +}; export -f kcreatecm function kgseckey() { local sec="$1"; shift local key="$1"; shift - kubectl get secret "$sec" -o jsonpath="{.data.$key}" | base64 -d -} + if ! kubectl get secret "$sec" -ojson | jq -re ".data.\"$key\" // empty" | base64 -d; then + return 1 + fi +}; export -f kgseckey function kgcmkey() { - local cm="$1"; shift + local cm="$1"; shift local key="$1"; shift - kubectl get configmap "$cm" -o jsonpath="{.data.$key}" -} + if ! kubectl get configmap "$cm" -ojson | jq -re ".data.\"$key\" // empty"; then + return 1 + fi +}; export -f kgcmkey kapply common/app.yaml diff --git a/website/manifests/bin/devel.sh b/website/manifests/bin/devel.sh index 464c4d0..65675aa 100755 --- a/website/manifests/bin/devel.sh +++ b/website/manifests/bin/devel.sh @@ -1,4 +1,5 @@ #!/bin/bash -e +set -o pipefail export NB_REPLICAS=1 diff --git a/website/manifests/bin/prod.sh b/website/manifests/bin/prod.sh index c97fc9e..b7b5f83 100755 --- a/website/manifests/bin/prod.sh +++ b/website/manifests/bin/prod.sh @@ -1,4 +1,5 @@ #!/bin/bash -e +set -o pipefail export NB_REPLICAS=3 diff --git a/website/open.sh b/website/open.sh new file mode 100755 index 0000000..f39b04f --- /dev/null +++ b/website/open.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +branch="$(git describe --contains --all HEAD)" + +xdg-open "https://$branch.g-eip-700-tls-7-1-eip-stephane.corbiere.icing.k8s.gmoker.com" diff --git a/website/src/main.go b/website/src/main.go new file mode 100644 index 0000000..823c600 --- /dev/null +++ b/website/src/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "log" + "net/http" +) + +func main() { + http.HandleFunc("/", route) + generateTmpl() + if err := http.ListenAndServe(":3000", nil); err != nil { + log.Fatal(err) + } +} diff --git a/website/src/route.go b/website/src/route.go new file mode 100644 index 0000000..3c9abe9 --- /dev/null +++ b/website/src/route.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "regexp" + "slices" +) + +type URLParam struct{} + +var routes = []struct { + methods []string + regex *regexp.Regexp + handler http.HandlerFunc +}{ + {[]string{"GET"}, url(""), index}, + {[]string{"GET"}, url("/static/.+"), static}, + {[]string{"GET"}, url("/(.+\\.css)"), css}, + {[]string{"GET"}, url("/([^/]+)"), html}, +} + +func url(s string) *regexp.Regexp { + return regexp.MustCompile("^" + s + "/?$") +} + +func getParam(r *http.Request, i int) string { + return r.Context().Value(URLParam{}).([]string)[i] +} + +func route(w http.ResponseWriter, r *http.Request) { + for _, rt := range routes { + matches := rt.regex.FindStringSubmatch(r.URL.Path) + if len(matches) > 0 { + if !slices.Contains(rt.methods, r.Method) { + w.Header().Set("Allow", r.Method) + http.Error( + w, "405 method not allowed", http.StatusMethodNotAllowed, + ) + return + } + fmt.Println(r.Method, r.URL.Path) + rt.handler(w, r.WithContext( + context.WithValue(r.Context(), URLParam{}, matches[1:]), + )) + return + } + } + http.NotFound(w, r) +} diff --git a/website/src/tmpl.go b/website/src/tmpl.go new file mode 100644 index 0000000..56de3e7 --- /dev/null +++ b/website/src/tmpl.go @@ -0,0 +1,31 @@ +package main + +import ( + "bytes" + "html/template" + "path/filepath" + "regexp" +) + +var TMPL map[string][]byte + +func generateTmpl() { + files, _ := filepath.Glob("html/*.html") + re := regexp.MustCompile("html/(.+).html") + pages := make([]string, len(files)) + + for i, f := range files { + pages[i] = re.FindStringSubmatch(f)[1] + } + TMPL = make(map[string][]byte, len(files)) + for i, f := range files { + b := new(bytes.Buffer) + t, _ := template.ParseFiles(f) + t.ParseGlob("tmpl/*.tmpl") + t.Execute(b, map[string]any{ + "page": pages[i], + "pages": pages, + }) + TMPL[pages[i]] = b.Bytes() + } +} diff --git a/website/src/views.go b/website/src/views.go new file mode 100644 index 0000000..a10c2bc --- /dev/null +++ b/website/src/views.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "net/http" + "path/filepath" +) + +func index(w http.ResponseWriter, r *http.Request) { + html(w, r.WithContext( + context.WithValue(r.Context(), URLParam{}, []string{"index"}), + )) +} + +func static(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, r.URL.Path) +} + +func css(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, filepath.Join("css", getParam(r, 0))) +} + +func html(w http.ResponseWriter, r *http.Request) { + if t, found := TMPL[getParam(r, 0)]; found { + w.Write(t) + } else { + http.NotFound(w, r) + } +} diff --git a/website/tmpl/head.tmpl b/website/tmpl/head.tmpl new file mode 100644 index 0000000..e04687c --- /dev/null +++ b/website/tmpl/head.tmpl @@ -0,0 +1,3 @@ +Icing + + diff --git a/website/tmpl/vars.tmpl b/website/tmpl/vars.tmpl new file mode 100644 index 0000000..fbc9680 --- /dev/null +++ b/website/tmpl/vars.tmpl @@ -0,0 +1,19 @@ +{{define "alexis"}} +Alexis DANLOS +{{end}} + +{{define "ange"}} +Ange DUHAYON +{{end}} + +{{define "bartosz"}} +Bartosz MICHALAK +{{end}} + +{{define "florian"}} +Florian GRIFFON +{{end}} + +{{define "stephane"}} +Stephane CORBIERE +{{end}} From cc849c83b14b0cec69e99e41e40a01a47fd7e41e Mon Sep 17 00:00:00 2001 From: stcb <21@stcb.cc> Date: Fri, 17 Jan 2025 14:08:10 +0200 Subject: [PATCH 2/7] Begin stealth mode prototype --- dialer/lib/features/composition/composition.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/dialer/lib/features/composition/composition.dart b/dialer/lib/features/composition/composition.dart index 488b4a5..37283fc 100644 --- a/dialer/lib/features/composition/composition.dart +++ b/dialer/lib/features/composition/composition.dart @@ -112,12 +112,14 @@ class _CompositionPageState extends State { : 'No phone number'; return ListTile( title: Text( - contact.displayName, + '${contact.displayName.isNotEmpty ? contact.displayName[0] : ''}...${contact.displayName.isNotEmpty ? contact.displayName[contact.displayName.length - 1] : ''}', + // contact.displayName style: const TextStyle(color: Colors.white), ), subtitle: Text( - phoneNumber, + '${phoneNumber.isNotEmpty ? phoneNumber[0] : ''}...${phoneNumber.isNotEmpty ? phoneNumber[phoneNumber.length - 1] : ''}', + // phoneNumber, style: const TextStyle(color: Colors.grey), ), @@ -149,12 +151,7 @@ class _CompositionPageState extends State { }, ); }).toList() - : [ - Center( - child: Text('No contacts found', - style: - TextStyle(color: Colors.white))) - ], + : [], ), ), ], From 475f4320477e93cff6f762d126bfb6c6fca6daf9 Mon Sep 17 00:00:00 2001 From: stcb <21@stcb.cc> Date: Fri, 17 Jan 2025 14:37:06 +0200 Subject: [PATCH 3/7] Moved Services to /services --- .../lib/features/composition/composition.dart | 98 ++++++++++--------- .../lib/features/contacts/contact_state.dart | 2 +- .../contacts/widgets/contact_modal.dart | 2 +- dialer/lib/features/home/home_page.dart | 2 +- .../{widgets => services}/block_service.dart | 0 .../contact_service.dart | 0 6 files changed, 53 insertions(+), 51 deletions(-) rename dialer/lib/{widgets => services}/block_service.dart (100%) rename dialer/lib/{widgets => services}/contact_service.dart (100%) diff --git a/dialer/lib/features/composition/composition.dart b/dialer/lib/features/composition/composition.dart index 37283fc..5be09be 100644 --- a/dialer/lib/features/composition/composition.dart +++ b/dialer/lib/features/composition/composition.dart @@ -1,7 +1,9 @@ +// lib/pages/composition_page.dart + import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; -import '../../widgets/contact_service.dart'; +import '../../services/contact_service.dart'; import '../contacts/widgets/add_contact_button.dart'; class CompositionPage extends StatefulWidget { @@ -107,50 +109,50 @@ class _CompositionPageState extends State { child: ListView( children: _filteredContacts.isNotEmpty ? _filteredContacts.map((contact) { - final phoneNumber = contact.phones.isNotEmpty - ? contact.phones.first.number - : 'No phone number'; - return ListTile( - title: Text( - '${contact.displayName.isNotEmpty ? contact.displayName[0] : ''}...${contact.displayName.isNotEmpty ? contact.displayName[contact.displayName.length - 1] : ''}', - // contact.displayName - style: - const TextStyle(color: Colors.white), - ), - subtitle: Text( - '${phoneNumber.isNotEmpty ? phoneNumber[0] : ''}...${phoneNumber.isNotEmpty ? phoneNumber[phoneNumber.length - 1] : ''}', - // phoneNumber, - style: - const TextStyle(color: Colors.grey), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // Call button - IconButton( - icon: Icon(Icons.phone, - color: Colors.green[300], - size: 20), - onPressed: () { - _launchPhoneDialer(phoneNumber); - }, - ), - // Message button - IconButton( - icon: Icon(Icons.message, - color: Colors.blue[300], - size: 20), - onPressed: () { - _launchSms(phoneNumber); - }, - ), - ], - ), - onTap: () { - // Handle contact selection if needed + final phoneNumber = contact.phones.isNotEmpty + ? contact.phones.first.number + : 'No phone number'; + return ListTile( + title: Text( + '${contact.displayName.isNotEmpty ? contact.displayName[0] : ''}...${contact.displayName.isNotEmpty ? contact.displayName[contact.displayName.length - 1] : ''}', + // contact.displayName + style: + const TextStyle(color: Colors.white), + ), + subtitle: Text( + '${phoneNumber.isNotEmpty ? phoneNumber[0] : ''}...${phoneNumber.isNotEmpty ? phoneNumber[phoneNumber.length - 1] : ''}', + // phoneNumber, + style: + const TextStyle(color: Colors.grey), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Call button + IconButton( + icon: Icon(Icons.phone, + color: Colors.green[300], + size: 20), + onPressed: () { + _launchPhoneDialer(phoneNumber); }, - ); - }).toList() + ), + // Message button + IconButton( + icon: Icon(Icons.message, + color: Colors.blue[300], + size: 20), + onPressed: () { + _launchSms(phoneNumber); + }, + ), + ], + ), + onTap: () { + // Handle contact selection if needed + }, + ); + }).toList() : [], ), ), @@ -199,7 +201,7 @@ class _CompositionPageState extends State { children: [ Row( mainAxisAlignment: - MainAxisAlignment.spaceEvenly, + MainAxisAlignment.spaceEvenly, children: [ _buildDialButton('1'), _buildDialButton('2'), @@ -208,7 +210,7 @@ class _CompositionPageState extends State { ), Row( mainAxisAlignment: - MainAxisAlignment.spaceEvenly, + MainAxisAlignment.spaceEvenly, children: [ _buildDialButton('4'), _buildDialButton('5'), @@ -217,7 +219,7 @@ class _CompositionPageState extends State { ), Row( mainAxisAlignment: - MainAxisAlignment.spaceEvenly, + MainAxisAlignment.spaceEvenly, children: [ _buildDialButton('7'), _buildDialButton('8'), @@ -226,7 +228,7 @@ class _CompositionPageState extends State { ), Row( mainAxisAlignment: - MainAxisAlignment.spaceEvenly, + MainAxisAlignment.spaceEvenly, children: [ _buildDialButton('*'), _buildDialButton('0'), diff --git a/dialer/lib/features/contacts/contact_state.dart b/dialer/lib/features/contacts/contact_state.dart index b6cf003..58e7574 100644 --- a/dialer/lib/features/contacts/contact_state.dart +++ b/dialer/lib/features/contacts/contact_state.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; -import '../../widgets/contact_service.dart'; +import '../../services/contact_service.dart'; class ContactState extends StatefulWidget { final Widget child; diff --git a/dialer/lib/features/contacts/widgets/contact_modal.dart b/dialer/lib/features/contacts/widgets/contact_modal.dart index 66cfdda..4f99974 100644 --- a/dialer/lib/features/contacts/widgets/contact_modal.dart +++ b/dialer/lib/features/contacts/widgets/contact_modal.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:dialer/widgets/username_color_generator.dart'; -import '../../../widgets/block_service.dart'; +import '../../../services/block_service.dart'; class ContactModal extends StatefulWidget { final Contact contact; diff --git a/dialer/lib/features/home/home_page.dart b/dialer/lib/features/home/home_page.dart index dbfc84f..23530a1 100644 --- a/dialer/lib/features/home/home_page.dart +++ b/dialer/lib/features/home/home_page.dart @@ -5,7 +5,7 @@ import 'package:dialer/features/history/history_page.dart'; import 'package:dialer/features/composition/composition.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:dialer/features/settings/settings.dart'; -import '../../widgets/contact_service.dart'; +import '../../services/contact_service.dart'; class _MyHomePageState extends State with SingleTickerProviderStateMixin { diff --git a/dialer/lib/widgets/block_service.dart b/dialer/lib/services/block_service.dart similarity index 100% rename from dialer/lib/widgets/block_service.dart rename to dialer/lib/services/block_service.dart diff --git a/dialer/lib/widgets/contact_service.dart b/dialer/lib/services/contact_service.dart similarity index 100% rename from dialer/lib/widgets/contact_service.dart rename to dialer/lib/services/contact_service.dart From e164f68bb8178b9efdb57e64db99335f4cda228b Mon Sep 17 00:00:00 2001 From: stcb <21@stcb.cc> Date: Sun, 26 Jan 2025 12:32:37 +0000 Subject: [PATCH 4/7] stealth-mode (#27) Reviewed-on: https://git.gmoker.com/icing/monorepo/pulls/27 --- dialer/android/app/build.gradle | 6 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- dialer/android/settings.gradle | 4 +- .../lib/features/composition/composition.dart | 16 +- .../widgets/alphabet_scroll_page.dart | 52 +++--- .../contacts/widgets/contact_modal.dart | 148 +++++++++--------- dialer/lib/features/history/history_page.dart | 54 ++++--- dialer/lib/features/home/home_page.dart | 5 +- dialer/lib/globals.dart | 1 + dialer/lib/main.dart | 9 +- dialer/lib/services/obfuscate_service.dart | 91 +++++++++++ dialer/pubspec.yaml | 1 - dialer/stealth_local_run.sh | 3 + dialer/test/widget_test.dart | 2 +- 14 files changed, 249 insertions(+), 145 deletions(-) create mode 100644 dialer/lib/globals.dart create mode 100644 dialer/lib/services/obfuscate_service.dart create mode 100755 dialer/stealth_local_run.sh diff --git a/dialer/android/app/build.gradle b/dialer/android/app/build.gradle index 0fcc76c..48dd268 100644 --- a/dialer/android/app/build.gradle +++ b/dialer/android/app/build.gradle @@ -11,12 +11,12 @@ android { ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_17 } defaultConfig { diff --git a/dialer/android/gradle/wrapper/gradle-wrapper.properties b/dialer/android/gradle/wrapper/gradle-wrapper.properties index 7bb2df6..22041e5 100644 --- a/dialer/android/gradle/wrapper/gradle-wrapper.properties +++ b/dialer/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip \ No newline at end of file diff --git a/dialer/android/settings.gradle b/dialer/android/settings.gradle index b9e43bd..b5e1b3f 100644 --- a/dialer/android/settings.gradle +++ b/dialer/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.0" apply false - id "org.jetbrains.kotlin.android" version "1.8.22" apply false + id "com.android.application" version "8.3.2" apply false + id "org.jetbrains.kotlin.android" version "2.0.20" apply false } include ":app" diff --git a/dialer/lib/features/composition/composition.dart b/dialer/lib/features/composition/composition.dart index 5be09be..6edfa01 100644 --- a/dialer/lib/features/composition/composition.dart +++ b/dialer/lib/features/composition/composition.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../services/contact_service.dart'; +import '../../services/obfuscate_service.dart'; // Import ObfuscateService import '../contacts/widgets/add_contact_button.dart'; class CompositionPage extends StatefulWidget { @@ -19,6 +20,9 @@ class _CompositionPageState extends State { List _filteredContacts = []; final ContactService _contactService = ContactService(); + // Instantiate the ObfuscateService + final ObfuscateService _obfuscateService = ObfuscateService(); + @override void initState() { super.initState(); @@ -114,16 +118,12 @@ class _CompositionPageState extends State { : 'No phone number'; return ListTile( title: Text( - '${contact.displayName.isNotEmpty ? contact.displayName[0] : ''}...${contact.displayName.isNotEmpty ? contact.displayName[contact.displayName.length - 1] : ''}', - // contact.displayName - style: - const TextStyle(color: Colors.white), + _obfuscateService.obfuscateData(contact.displayName), + style: const TextStyle(color: Colors.white), ), subtitle: Text( - '${phoneNumber.isNotEmpty ? phoneNumber[0] : ''}...${phoneNumber.isNotEmpty ? phoneNumber[phoneNumber.length - 1] : ''}', - // phoneNumber, - style: - const TextStyle(color: Colors.grey), + _obfuscateService.obfuscateData(phoneNumber), + style: const TextStyle(color: Colors.grey), ), trailing: Row( mainAxisSize: MainAxisSize.min, diff --git a/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart b/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart index c144084..4595b24 100644 --- a/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart +++ b/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart @@ -1,8 +1,10 @@ +// alphabet_scrollpage.dart +import 'dart:ui'; +import 'package:dialer/services/obfuscate_service.dart'; import 'package:dialer/widgets/username_color_generator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import '../contact_state.dart'; -import '../../../widgets/color_darkener.dart'; import 'add_contact_button.dart'; import 'contact_modal.dart'; import 'share_own_qr.dart'; @@ -24,6 +26,8 @@ class AlphabetScrollPage extends StatefulWidget { class _AlphabetScrollPageState extends State { late ScrollController _scrollController; + final ObfuscateService _obfuscateService = ObfuscateService(); + @override void initState() { super.initState(); @@ -124,7 +128,7 @@ class _AlphabetScrollPageState extends State { vertical: 8.0, horizontal: 16.0), child: Text( letter, - style: TextStyle( + style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, color: Colors.white, @@ -134,31 +138,25 @@ class _AlphabetScrollPageState extends State { // Contact Entries ...contactsForLetter.map((contact) { String phoneNumber = contact.phones.isNotEmpty - ? contact.phones.first.number + ? _obfuscateService.obfuscateData(contact.phones.first.number) : 'No phone number'; Color avatarColor = - generateColorFromName(contact.displayName); + generateColorFromName(contact.displayName); return ListTile( - leading: (contact.thumbnail != null && - contact.thumbnail!.isNotEmpty) - ? CircleAvatar( - backgroundImage: - MemoryImage(contact.thumbnail!), - ) - : CircleAvatar( - backgroundColor: avatarColor, - child: Text( - contact.displayName.isNotEmpty - ? contact.displayName[0].toUpperCase() - : '?', - style: TextStyle( - color: darken(avatarColor, 0.4)), - ), - ), - title: Text(contact.displayName, - style: TextStyle(color: Colors.white)), - subtitle: Text(phoneNumber, - style: TextStyle(color: Colors.white70)), + leading: ObfuscatedAvatar( + imageBytes: contact.thumbnail, + radius: 25, + backgroundColor: avatarColor, + fallbackInitial: contact.displayName, + ), + title: Text( + _obfuscateService.obfuscateData(contact.displayName), + style: const TextStyle(color: Colors.white), + ), + subtitle: Text( + phoneNumber, + style: const TextStyle(color: Colors.white70), + ), onTap: () { showModalBottomSheet( context: context, @@ -170,8 +168,8 @@ class _AlphabetScrollPageState extends State { onEdit: () async { if (await FlutterContacts.requestPermission()) { final updatedContact = - await FlutterContacts.openExternalEdit( - contact.id); + await FlutterContacts.openExternalEdit( + contact.id); if (updatedContact != null) { await _refreshContacts(); Navigator.of(context).pop(); @@ -187,7 +185,7 @@ class _AlphabetScrollPageState extends State { .showSnackBar( SnackBar( content: - Text('Edit canceled or failed.'), + Text('Edit canceled or failed.'), ), ); } diff --git a/dialer/lib/features/contacts/widgets/contact_modal.dart b/dialer/lib/features/contacts/widgets/contact_modal.dart index 4f99974..fe42bb2 100644 --- a/dialer/lib/features/contacts/widgets/contact_modal.dart +++ b/dialer/lib/features/contacts/widgets/contact_modal.dart @@ -1,3 +1,6 @@ +import 'dart:ui'; + +import 'package:dialer/services/obfuscate_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -25,6 +28,7 @@ class ContactModal extends StatefulWidget { class _ContactModalState extends State { late String phoneNumber; bool isBlocked = false; + final ObfuscateService _obfuscateService = ObfuscateService(); @override void initState() { @@ -94,50 +98,51 @@ class _ContactModalState extends State { } } -void _deleteContact() async { - final bool shouldDelete = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Delete Contact'), - content: Text('Are you sure you want to delete ${widget.contact.displayName}?'), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('Delete'), - ), - ], - ), - ); + void _deleteContact() async { + final bool shouldDelete = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Delete Contact'), + content: Text( + 'Are you sure you want to delete ${_obfuscateService.obfuscateData(widget.contact.displayName)}?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Delete'), + ), + ], + ), + ); - if (shouldDelete) { - try { - // Delete the contact - await FlutterContacts.deleteContact(widget.contact); + if (shouldDelete) { + try { + // Delete the contact + await FlutterContacts.deleteContact(widget.contact); - // Show success message - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('${widget.contact.displayName} deleted')), - ); + // Show success message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')), + ); - // Close the modal - Navigator.of(context).pop(); - } catch (e) { - // Handle errors and show a failure message - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Failed to delete ${widget.contact.displayName}: $e')), - ); + // Close the modal + Navigator.of(context).pop(); + } catch (e) { + // Handle errors and show a failure message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to delete ${widget.contact.displayName}: $e')), + ); + } } } -} @override Widget build(BuildContext context) { String email = widget.contact.emails.isNotEmpty - ? widget.contact.emails.first.address + ? _obfuscateService.obfuscateData(widget.contact.emails.first.address) : 'No email'; return GestureDetector( @@ -151,7 +156,7 @@ void _deleteContact() async { decoration: BoxDecoration( color: Colors.grey[900], borderRadius: - const BorderRadius.vertical(top: Radius.circular(20)), + const BorderRadius.vertical(top: Radius.circular(20)), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -184,6 +189,7 @@ void _deleteContact() async { if (choice == 'delete') { _deleteContact(); } + // Handle other choices if needed }, itemBuilder: (BuildContext context) { return [ @@ -202,7 +208,7 @@ void _deleteContact() async { const PopupMenuItem( value: 'create_shortcut', child: - Text('Create shortcut (to home screen)'), + Text('Create shortcut (to home screen)'), ), const PopupMenuItem( value: 'set_ringtone', @@ -220,40 +226,32 @@ void _deleteContact() async { padding: const EdgeInsets.all(16.0), child: Column( children: [ - CircleAvatar( + ObfuscatedAvatar( + imageBytes: widget.contact.thumbnail, radius: 50, - backgroundImage: (widget.contact.thumbnail != null && - widget.contact.thumbnail!.isNotEmpty) - ? MemoryImage(widget.contact.thumbnail!) - : null, backgroundColor: - generateColorFromName(widget.contact.displayName), - child: (widget.contact.thumbnail == null || - widget.contact.thumbnail!.isEmpty) - ? Text( - widget.contact.displayName.isNotEmpty - ? widget.contact.displayName[0] - .toUpperCase() - : '?', - style: const TextStyle( - fontSize: 40, color: Colors.white), - ) - : null, + generateColorFromName(widget.contact.displayName), + fallbackInitial: widget.contact.displayName, ), const SizedBox(height: 10), Text( - widget.contact.displayName, + _obfuscateService.obfuscateData(widget.contact.displayName), style: const TextStyle( - fontSize: 24, fontWeight: FontWeight.bold), + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white), ), ], ), ), - const Divider(), + const Divider(color: Colors.grey), // Contact Actions ListTile( leading: const Icon(Icons.phone, color: Colors.green), - title: Text(phoneNumber), + title: Text( + _obfuscateService.obfuscateData(phoneNumber), + style: const TextStyle(color: Colors.white), + ), onTap: () { if (widget.contact.phones.isNotEmpty) { _launchPhoneDialer(phoneNumber); @@ -262,7 +260,10 @@ void _deleteContact() async { ), ListTile( leading: const Icon(Icons.message, color: Colors.blue), - title: Text(phoneNumber), + title: Text( + _obfuscateService.obfuscateData(phoneNumber), + style: const TextStyle(color: Colors.white), + ), onTap: () { if (widget.contact.phones.isNotEmpty) { _launchSms(phoneNumber); @@ -271,14 +272,17 @@ void _deleteContact() async { ), ListTile( leading: const Icon(Icons.email, color: Colors.orange), - title: Text(email), + title: Text( + email, + style: const TextStyle(color: Colors.white), + ), onTap: () { if (widget.contact.emails.isNotEmpty) { _launchEmail(email); } }, ), - const Divider(), + const Divider(color: Colors.grey), // Favorite, Edit, and Block/Unblock Buttons Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), @@ -286,8 +290,7 @@ void _deleteContact() async { children: [ // Favorite button SizedBox( - width: double - .infinity, // This makes the button take full width + width: double.infinity, child: ElevatedButton.icon( onPressed: () { Navigator.of(context).pop(); @@ -296,35 +299,32 @@ void _deleteContact() async { icon: Icon(widget.isFavorite ? Icons.star : Icons.star_border), - label: Text( - widget.isFavorite ? 'Unfavorite' : 'Favorite'), + label: Text(widget.isFavorite + ? 'Unfavorite' + : 'Favorite'), ), ), - const SizedBox(height: 10), // Space between buttons - + const SizedBox(height: 10), // Edit button SizedBox( - width: double - .infinity, // This makes the button take full width + width: double.infinity, child: ElevatedButton.icon( onPressed: () => widget.onEdit(), icon: const Icon(Icons.edit), label: const Text('Edit Contact'), ), ), - const SizedBox(height: 10), // Space between buttons - + const SizedBox(height: 10), // Block/Unblock button SizedBox( - width: double - .infinity, // This makes the button take full width + width: double.infinity, child: ElevatedButton.icon( onPressed: _toggleBlockState, icon: Icon( isBlocked ? Icons.block : Icons.block_flipped), label: Text(isBlocked ? 'Unblock' : 'Block'), + ), ), - ), ], ), ), diff --git a/dialer/lib/features/history/history_page.dart b/dialer/lib/features/history/history_page.dart index 589a136..b8c0567 100644 --- a/dialer/lib/features/history/history_page.dart +++ b/dialer/lib/features/history/history_page.dart @@ -1,10 +1,11 @@ +import 'package:dialer/services/obfuscate_service.dart'; +import 'package:dialer/widgets/color_darkener.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:dialer/features/contacts/contact_state.dart'; import 'package:dialer/widgets/username_color_generator.dart'; -import 'package:dialer/widgets/color_darkener.dart'; class History { final Contact contact; @@ -34,6 +35,8 @@ class _HistoryPageState extends State with SingleTickerProviderStat bool loading = true; int? _expandedIndex; + final ObfuscateService _obfuscateService = ObfuscateService(); + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -206,21 +209,21 @@ class _HistoryPageState extends State with SingleTickerProviderStat return Column( children: [ ListTile( - leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty) - ? CircleAvatar( - backgroundImage: MemoryImage(contact.thumbnail!), - ) - : CircleAvatar( - backgroundColor: avatarColor, - child: Text( - contact.displayName.isNotEmpty - ? contact.displayName[0].toUpperCase() - : '?', - style: TextStyle(color: darken(avatarColor, 0.4)), + leading: ObfuscatedAvatar( + imageBytes: contact.thumbnail, + radius: 25, + backgroundColor: avatarColor, + fallbackInitial: contact.displayName, ), - ), + // child: Text( + // contact.displayName.isNotEmpty + // ? contact.displayName[0].toUpperCase() + // : '?', + // style: TextStyle(color: darken(avatarColor, 0.4)), + // ), + // ), title: Text( - contact.displayName, + _obfuscateService.obfuscateData(contact.displayName), style: const TextStyle(color: Colors.white), ), subtitle: Text( @@ -329,12 +332,15 @@ class _HistoryPageState extends State with SingleTickerProviderStat class CallDetailsPage extends StatelessWidget { final History history; + final ObfuscateService _obfuscateService = ObfuscateService(); - const CallDetailsPage({Key? key, required this.history}) : super(key: key); + CallDetailsPage({super.key, required this.history}); @override Widget build(BuildContext context) { final contact = history.contact; + final contactBg = generateColorFromName(contact.displayName); + final contactLetter = darken(contactBg); return Scaffold( backgroundColor: Colors.black, @@ -350,24 +356,26 @@ class CallDetailsPage extends StatelessWidget { Row( children: [ (contact.thumbnail != null && contact.thumbnail!.isNotEmpty) - ? CircleAvatar( - backgroundImage: MemoryImage(contact.thumbnail!), - radius: 30, - ) + ? ObfuscatedAvatar( + imageBytes: contact.thumbnail, + radius: 30, + backgroundColor: contactBg, + fallbackInitial: contact.displayName, + ) : CircleAvatar( - backgroundColor: Colors.grey[700], + backgroundColor: generateColorFromName(contact.displayName), radius: 30, child: Text( contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '?', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: contactLetter), ), ), const SizedBox(width: 16), Expanded( child: Text( - contact.displayName, + _obfuscateService.obfuscateData(contact.displayName), style: const TextStyle(color: Colors.white, fontSize: 24), ), ), @@ -399,7 +407,7 @@ class CallDetailsPage extends StatelessWidget { if (contact.phones.isNotEmpty) DetailRow( label: 'Number:', - value: contact.phones.first.number, + value: _obfuscateService.obfuscateData(contact.phones.first.number), ), ], ), diff --git a/dialer/lib/features/home/home_page.dart b/dialer/lib/features/home/home_page.dart index 23530a1..ac55cf8 100644 --- a/dialer/lib/features/home/home_page.dart +++ b/dialer/lib/features/home/home_page.dart @@ -1,3 +1,4 @@ +import 'package:dialer/services/obfuscate_service.dart'; import 'package:flutter/material.dart'; import 'package:dialer/features/contacts/contact_page.dart'; import 'package:dialer/features/favorites/favorites_page.dart'; @@ -13,7 +14,7 @@ class _MyHomePageState extends State List _allContacts = []; List _contactSuggestions = []; final ContactService _contactService = ContactService(); - + final ObfuscateService _obfuscateService = ObfuscateService(); @override void initState() { @@ -130,7 +131,7 @@ class _MyHomePageState extends State return _contactSuggestions.map((contact) { return ListTile( key: ValueKey(contact.id), - title: Text(contact.displayName, + title: Text(_obfuscateService.obfuscateData(contact.displayName), style: const TextStyle(color: Colors.white)), onTap: () { controller.closeView(contact.displayName); diff --git a/dialer/lib/globals.dart b/dialer/lib/globals.dart new file mode 100644 index 0000000..2750ab2 --- /dev/null +++ b/dialer/lib/globals.dart @@ -0,0 +1 @@ +bool isStealthMode = false; \ No newline at end of file diff --git a/dialer/lib/main.dart b/dialer/lib/main.dart index a9ca957..1f760e8 100644 --- a/dialer/lib/main.dart +++ b/dialer/lib/main.dart @@ -1,13 +1,16 @@ import 'package:dialer/features/home/home_page.dart'; import 'package:flutter/material.dart'; import 'package:dialer/features/contacts/contact_state.dart'; +import 'globals.dart' as globals; void main() { - runApp(const MyApp()); + const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false'); + globals.isStealthMode = stealthFlag.toLowerCase() == 'true'; + runApp(const Dialer()); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class Dialer extends StatelessWidget { + const Dialer({super.key}); @override Widget build(BuildContext context) { diff --git a/dialer/lib/services/obfuscate_service.dart b/dialer/lib/services/obfuscate_service.dart new file mode 100644 index 0000000..6eaf43f --- /dev/null +++ b/dialer/lib/services/obfuscate_service.dart @@ -0,0 +1,91 @@ +// lib/services/obfuscate_service.dart +import 'package:dialer/widgets/color_darkener.dart'; + +import '../../globals.dart' as globals; +import 'dart:ui'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; + +class ObfuscateService { + // Private constructor + ObfuscateService._privateConstructor(); + + // Singleton instance + static final ObfuscateService _instance = ObfuscateService._privateConstructor(); + + // Factory constructor to return the same instance + factory ObfuscateService() { + return _instance; + } + + // Public method to obfuscate data + String obfuscateData(String data) { + if (globals.isStealthMode) { + return _obfuscateData(data); + } else { + return data; + } + } + + // Private helper method for obfuscation logic + String _obfuscateData(String data) { + if (data.isNotEmpty) { + // Ensure the string has at least two characters to obfuscate + if (data.length == 1) { + return '${data[0]}'; + } else { + return '${data[0]}...${data[data.length - 1]}'; + } + } + return ''; + } +} + + +class ObfuscatedAvatar extends StatelessWidget { + final Uint8List? imageBytes; + final double radius; + final Color backgroundColor; + final String? fallbackInitial; + + const ObfuscatedAvatar({ + Key? key, + required this.imageBytes, + this.radius = 25, + this.backgroundColor = Colors.grey, + this.fallbackInitial, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (imageBytes != null && imageBytes!.isNotEmpty) { + return ClipOval( + child: ImageFiltered( + imageFilter: globals.isStealthMode + ? ImageFilter.blur(sigmaX: 10, sigmaY: 10) + : ImageFilter.blur(sigmaX: 0, sigmaY: 0), + child: Image.memory( + imageBytes!, + fit: BoxFit.cover, + width: radius * 2, + height: radius * 2, + ), + ), + ); + } else { + return CircleAvatar( + radius: radius, + backgroundColor: backgroundColor, + child: Text( + fallbackInitial != null && fallbackInitial!.isNotEmpty + ? fallbackInitial![0].toUpperCase() + : '?', + style: TextStyle( + color: darken(backgroundColor), + fontSize: radius, + ), + ), + ); + } + } +} diff --git a/dialer/pubspec.yaml b/dialer/pubspec.yaml index 6e36f8c..33623b4 100644 --- a/dialer/pubspec.yaml +++ b/dialer/pubspec.yaml @@ -40,7 +40,6 @@ dependencies: permission_handler: ^11.3.1 # For handling permissions cached_network_image: ^3.2.3 # For caching contact images qr_flutter: ^4.1.0 - android_intent_plus: ^5.2.0 camera: ^0.10.0+2 mobile_scanner: ^6.0.2 pretty_qr_code: ^3.3.0 diff --git a/dialer/stealth_local_run.sh b/dialer/stealth_local_run.sh new file mode 100755 index 0000000..95cc270 --- /dev/null +++ b/dialer/stealth_local_run.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +echo "Running Icing Dialer in STEALTH mode..." +flutter run --dart-define=STEALTH=true \ No newline at end of file diff --git a/dialer/test/widget_test.dart b/dialer/test/widget_test.dart index 7b0e078..15b52a2 100644 --- a/dialer/test/widget_test.dart +++ b/dialer/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:dialer/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const Dialer()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); From ee1f2ee4b176714762dd79e41198382d161ff881 Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Thu, 30 Jan 2025 14:15:07 +0000 Subject: [PATCH 5/7] feat: can share any contact using qr code (#30) Reviewed-on: https://git.gmoker.com/icing/monorepo/pulls/30 Co-authored-by: Florian Griffon Co-committed-by: Florian Griffon --- .../widgets/alphabet_scroll_page.dart | 2 -- .../contacts/widgets/contact_modal.dart | 10 ++++-- .../contacts/widgets/share_own_qr.dart | 27 +++------------ dialer/lib/services/contact_service.dart | 34 +++++++++++++++---- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart b/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart index 4595b24..822c36e 100644 --- a/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart +++ b/dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart @@ -1,5 +1,3 @@ -// alphabet_scrollpage.dart -import 'dart:ui'; import 'package:dialer/services/obfuscate_service.dart'; import 'package:dialer/widgets/username_color_generator.dart'; import 'package:flutter/material.dart'; diff --git a/dialer/lib/features/contacts/widgets/contact_modal.dart b/dialer/lib/features/contacts/widgets/contact_modal.dart index fe42bb2..632d8be 100644 --- a/dialer/lib/features/contacts/widgets/contact_modal.dart +++ b/dialer/lib/features/contacts/widgets/contact_modal.dart @@ -1,11 +1,10 @@ -import 'dart:ui'; - import 'package:dialer/services/obfuscate_service.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:dialer/widgets/username_color_generator.dart'; import '../../../services/block_service.dart'; +import '../../../services/contact_service.dart'; class ContactModal extends StatefulWidget { final Contact contact; @@ -139,6 +138,11 @@ class _ContactModalState extends State { } } + void _shareContactAsQRCode() { + // Use the ContactService to show the QR code for the contact's vCard + ContactService().showContactQRCodeDialog(context, widget.contact); + } + @override Widget build(BuildContext context) { String email = widget.contact.emails.isNotEmpty @@ -188,6 +192,8 @@ class _ContactModalState extends State { onSelected: (String choice) { if (choice == 'delete') { _deleteContact(); + } else if (choice == 'share') { + _shareContactAsQRCode(); } // Handle other choices if needed }, diff --git a/dialer/lib/features/contacts/widgets/share_own_qr.dart b/dialer/lib/features/contacts/widgets/share_own_qr.dart index ef16c27..058ed35 100644 --- a/dialer/lib/features/contacts/widgets/share_own_qr.dart +++ b/dialer/lib/features/contacts/widgets/share_own_qr.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/contact.dart'; -import 'package:qr_flutter/qr_flutter.dart'; +import 'package:dialer/services/contact_service.dart'; class QRCodeButton extends StatelessWidget { final List contacts; @@ -23,32 +23,13 @@ class QRCodeButton extends StatelessWidget { @override Widget build(BuildContext context) { - return IconButton( icon: Icon(Icons.qr_code, color: selfContact != null ? Colors.blue : Colors.grey), onPressed: selfContact != null ? () { - showDialog( - barrierColor: Colors.white24, - context: context, - barrierDismissible: true, - builder: (BuildContext context) { - return AlertDialog( - backgroundColor: Colors.black, - content: SizedBox( - width: 200, - height: 220, - child: QrImageView( - data: selfContact!.toVCard(), - version: QrVersions.auto, - backgroundColor: Colors.white, // Ensure QR code is visible on black background - size: 200.0, - ), - ), - ); - }, - ); - } + // Use the ContactService to show the QR code + ContactService().showContactQRCodeDialog(context, selfContact!); + } : null, ); } diff --git a/dialer/lib/services/contact_service.dart b/dialer/lib/services/contact_service.dart index 61d365d..ef00185 100644 --- a/dialer/lib/services/contact_service.dart +++ b/dialer/lib/services/contact_service.dart @@ -1,4 +1,6 @@ +import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; +import 'package:qr_flutter/qr_flutter.dart'; // Service to manage contact-related operations class ContactService { @@ -15,17 +17,35 @@ class ContactService { } Future> fetchFavoriteContacts() async { - // Fetch all contacts List contacts = await fetchContacts(); - - // Filter contacts to only include those with isStarred: true - List favoriteContacts = - contacts.where((contact) => contact.isStarred).toList(); - - return favoriteContacts; + return contacts.where((contact) => contact.isStarred).toList(); } Future addNewContact(Contact contact) async { await FlutterContacts.insertContact(contact); } + + // Function to show an AlertDialog with a QR code for the contact's vCard + void showContactQRCodeDialog(BuildContext context, Contact contact) { + showDialog( + barrierColor: Colors.white24, + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.black, + content: SizedBox( + width: 200, + height: 220, + child: QrImageView( + data: contact.toVCard(), // Generate vCard QR code + version: QrVersions.auto, + backgroundColor: Colors.white, // Make sure QR code is visible on black background + size: 200.0, + ), + ), + ); + }, + ); + } } From f99fdaa1609ac6c7cb912f0957676e4ed8f7de6d Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Thu, 30 Jan 2025 14:25:04 +0000 Subject: [PATCH 6/7] feat: can block/unblock in history page (#29) Reviewed-on: https://git.gmoker.com/icing/monorepo/pulls/29 Co-authored-by: Florian Griffon Co-committed-by: Florian Griffon --- dialer/lib/features/history/history_page.dart | 216 +++++++++++------- 1 file changed, 131 insertions(+), 85 deletions(-) diff --git a/dialer/lib/features/history/history_page.dart b/dialer/lib/features/history/history_page.dart index b8c0567..642b942 100644 --- a/dialer/lib/features/history/history_page.dart +++ b/dialer/lib/features/history/history_page.dart @@ -6,6 +6,7 @@ import 'package:intl/intl.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:dialer/features/contacts/contact_state.dart'; import 'package:dialer/widgets/username_color_generator.dart'; +import '../../services/block_service.dart'; class History { final Contact contact; @@ -15,12 +16,12 @@ class History { final int attempts; History( - this.contact, - this.date, - this.callType, - this.callStatus, - this.attempts, - ); + this.contact, + this.date, + this.callType, + this.callStatus, + this.attempts, + ); } class HistoryPage extends StatefulWidget { @@ -30,7 +31,8 @@ class HistoryPage extends StatefulWidget { _HistoryPageState createState() => _HistoryPageState(); } -class _HistoryPageState extends State with SingleTickerProviderStateMixin { +class _HistoryPageState extends State + with SingleTickerProviderStateMixin { List histories = []; bool loading = true; int? _expandedIndex; @@ -66,7 +68,7 @@ class _HistoryPageState extends State with SingleTickerProviderStat setState(() { histories = List.generate( contacts.length >= 10 ? 10 : contacts.length, - (index) => History( + (index) => History( contacts[index], DateTime.now().subtract(Duration(hours: (index + 1) * 2)), index % 2 == 0 ? 'outgoing' : 'incoming', @@ -91,7 +93,8 @@ class _HistoryPageState extends State with SingleTickerProviderStat List olderHistories = []; for (var history in historyList) { - final callDate = DateTime(history.date.year, history.date.month, history.date.day); + final callDate = + DateTime(history.date.year, history.date.month, history.date.day); if (callDate == today) { todayHistories.add(history); } else if (callDate == yesterday) { @@ -145,7 +148,8 @@ class _HistoryPageState extends State with SingleTickerProviderStat } // Filter missed calls - List missedCalls = histories.where((h) => h.callStatus == 'missed').toList(); + List missedCalls = + histories.where((h) => h.callStatus == 'missed').toList(); final allItems = _buildGroupedList(histories); final missedItems = _buildGroupedList(missedCalls); @@ -211,17 +215,10 @@ class _HistoryPageState extends State with SingleTickerProviderStat ListTile( leading: ObfuscatedAvatar( imageBytes: contact.thumbnail, - radius: 25, - backgroundColor: avatarColor, - fallbackInitial: contact.displayName, - ), - // child: Text( - // contact.displayName.isNotEmpty - // ? contact.displayName[0].toUpperCase() - // : '?', - // style: TextStyle(color: darken(avatarColor, 0.4)), - // ), - // ), + radius: 25, + backgroundColor: avatarColor, + fallbackInitial: contact.displayName, + ), title: Text( _obfuscateService.obfuscateData(contact.displayName), style: const TextStyle(color: Colors.white), @@ -241,18 +238,20 @@ class _HistoryPageState extends State with SingleTickerProviderStat icon: const Icon(Icons.phone, color: Colors.green), onPressed: () async { if (contact.phones.isNotEmpty) { - final Uri callUri = - Uri(scheme: 'tel', path: contact.phones.first.number); + final Uri callUri = Uri( + scheme: 'tel', path: contact.phones.first.number); if (await canLaunchUrl(callUri)) { await launchUrl(callUri); } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Could not launch call')), + const SnackBar( + content: Text('Could not launch call')), ); } } else { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Contact has no phone number')), + const SnackBar( + content: Text('Contact has no phone number')), ); } }, @@ -268,56 +267,99 @@ class _HistoryPageState extends State with SingleTickerProviderStat if (isExpanded) Container( color: Colors.grey[850], - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - TextButton.icon( - onPressed: () async { - if (history.contact.phones.isNotEmpty) { - final Uri smsUri = - Uri(scheme: 'sms', path: history.contact.phones.first.number); - if (await canLaunchUrl(smsUri)) { - await launchUrl(smsUri); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Could not send message')), + child: FutureBuilder( + future: BlockService().isNumberBlocked( + contact.phones.isNotEmpty + ? contact.phones.first.number + : ''), + builder: (context, snapshot) { + final isBlocked = snapshot.data ?? false; + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + TextButton.icon( + onPressed: () async { + if (history.contact.phones.isNotEmpty) { + final Uri smsUri = Uri( + scheme: 'sms', + path: history.contact.phones.first.number); + if (await canLaunchUrl(smsUri)) { + await launchUrl(smsUri); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Could not send message')), + ); + } + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Contact has no phone number')), + ); + } + }, + icon: + const Icon(Icons.message, color: Colors.white), + label: const Text('Message', + style: TextStyle(color: Colors.white)), + ), + TextButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => + CallDetailsPage(history: history), + ), ); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Contact has no phone number')), - ); - } - }, - icon: const Icon(Icons.message, color: Colors.white), - label: const Text('Message', style: TextStyle(color: Colors.white)), - ), - TextButton.icon( - onPressed: () { - // Navigate to Call Details page - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => CallDetailsPage(history: history), + }, + icon: const Icon(Icons.info, color: Colors.white), + label: const Text('Details', + style: TextStyle(color: Colors.white)), + ), + TextButton.icon( + onPressed: () async { + final phoneNumber = contact.phones.isNotEmpty + ? contact.phones.first.number + : null; + if (phoneNumber == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Contact has no phone number'), + ), + ); + return; + } + + if (isBlocked) { + await BlockService().unblockNumber(phoneNumber); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('$phoneNumber unblocked')), + ); + } else { + await BlockService().blockNumber(phoneNumber); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('$phoneNumber blocked')), + ); + } + setState(() {}); // Refresh the button state + }, + icon: Icon( + isBlocked ? Icons.lock_open : Icons.block, + color: Colors.white), + label: Text( + isBlocked ? 'Unblock' : 'Block', + style: const TextStyle(color: Colors.white), ), - ); - }, - icon: const Icon(Icons.info, color: Colors.white), - label: const Text('Details', style: TextStyle(color: Colors.white)), - ), - TextButton.icon( - onPressed: () { - // Implement block number functionality - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Number blocked (functionality not implemented)'), - ), - ); - }, - icon: const Icon(Icons.block, color: Colors.white), - label: const Text('Block', style: TextStyle(color: Colors.white)), - ), - ], + ), + ], + ); + }, ), ), ], @@ -363,15 +405,16 @@ class CallDetailsPage extends StatelessWidget { fallbackInitial: contact.displayName, ) : CircleAvatar( - backgroundColor: generateColorFromName(contact.displayName), - radius: 30, - child: Text( - contact.displayName.isNotEmpty - ? contact.displayName[0].toUpperCase() - : '?', - style: TextStyle(color: contactLetter), - ), - ), + backgroundColor: + generateColorFromName(contact.displayName), + radius: 30, + child: Text( + contact.displayName.isNotEmpty + ? contact.displayName[0].toUpperCase() + : '?', + style: TextStyle(color: contactLetter), + ), + ), const SizedBox(width: 16), Expanded( child: Text( @@ -407,7 +450,8 @@ class CallDetailsPage extends StatelessWidget { if (contact.phones.isNotEmpty) DetailRow( label: 'Number:', - value: _obfuscateService.obfuscateData(contact.phones.first.number), + value: _obfuscateService + .obfuscateData(contact.phones.first.number), ), ], ), @@ -420,7 +464,8 @@ class DetailRow extends StatelessWidget { final String label; final String value; - const DetailRow({Key? key, required this.label, required this.value}) : super(key: key); + const DetailRow({Key? key, required this.label, required this.value}) + : super(key: key); @override Widget build(BuildContext context) { @@ -430,7 +475,8 @@ class DetailRow extends StatelessWidget { children: [ Text( label, - style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.bold), + style: const TextStyle( + color: Colors.white70, fontWeight: FontWeight.bold), ), const SizedBox(width: 8), Expanded( From f3f5c7062057eedccd3652a4240b3dd1ba7f3c1b Mon Sep 17 00:00:00 2001 From: Florian Griffon Date: Thu, 30 Jan 2025 14:47:24 +0000 Subject: [PATCH 7/7] feat: History page has contact modal on profile picture (#31) Reviewed-on: https://git.gmoker.com/icing/monorepo/pulls/31 Co-authored-by: Florian Griffon Co-committed-by: Florian Griffon --- dialer/lib/features/history/history_page.dart | 103 ++++++++++++++++-- 1 file changed, 91 insertions(+), 12 deletions(-) diff --git a/dialer/lib/features/history/history_page.dart b/dialer/lib/features/history/history_page.dart index 642b942..6054173 100644 --- a/dialer/lib/features/history/history_page.dart +++ b/dialer/lib/features/history/history_page.dart @@ -7,6 +7,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:dialer/features/contacts/contact_state.dart'; import 'package:dialer/widgets/username_color_generator.dart'; import '../../services/block_service.dart'; +import '../contacts/widgets/contact_modal.dart'; class History { final Contact contact; @@ -47,6 +48,45 @@ class _HistoryPageState extends State } } + Future _refreshContacts() async { + final contactState = ContactState.of(context); + try { + // Refresh contacts or fetch them again + await contactState.fetchContacts(); + } catch (e) { + print('Error refreshing contacts: $e'); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to refresh contacts')), + ); + } + } + + void _toggleFavorite(Contact contact) async { + try { + // Ensure you have the necessary permissions to fetch contact details + if (await FlutterContacts.requestPermission()) { + Contact? fullContact = await FlutterContacts.getContact(contact.id, + withProperties: true, + withAccounts: true, + withPhoto: true, + withThumbnail: true); + + if (fullContact != null) { + fullContact.isStarred = !fullContact.isStarred; + await FlutterContacts.updateContact(fullContact); + } + await _refreshContacts(); // Refresh the contact list + } else { + print("Could not fetch contact details"); + } + } catch (e) { + print("Error updating favorite status: $e"); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Failed to update contact favorite status')), + ); + } + } + Future _buildHistories() async { final contactState = ContactState.of(context); if (contactState.loading) { @@ -213,11 +253,53 @@ class _HistoryPageState extends State return Column( children: [ ListTile( - leading: ObfuscatedAvatar( - imageBytes: contact.thumbnail, - radius: 25, - backgroundColor: avatarColor, - fallbackInitial: contact.displayName, + leading: GestureDetector( + onTap: () { + // When the profile picture is tapped, show the ContactModal + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) { + return ContactModal( + contact: contact, + onEdit: () async { + if (await FlutterContacts.requestPermission()) { + final updatedContact = + await FlutterContacts.openExternalEdit( + contact.id); + if (updatedContact != null) { + await _refreshContacts(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + '${contact.displayName} updated successfully!'), + ), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Edit canceled or failed.'), + ), + ); + } + } + }, + onToggleFavorite: () { + _toggleFavorite(contact); + }, + isFavorite: contact.isStarred, + ); + }, + ); + }, + child: ObfuscatedAvatar( + imageBytes: contact.thumbnail, + radius: 25, + backgroundColor: avatarColor, + fallbackInitial: contact.displayName, + ), ), title: Text( _obfuscateService.obfuscateData(contact.displayName), @@ -327,9 +409,8 @@ class _HistoryPageState extends State if (phoneNumber == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: - Text('Contact has no phone number'), - ), + content: + Text('Contact has no phone number')), ); return; } @@ -352,10 +433,8 @@ class _HistoryPageState extends State icon: Icon( isBlocked ? Icons.lock_open : Icons.block, color: Colors.white), - label: Text( - isBlocked ? 'Unblock' : 'Block', - style: const TextStyle(color: Colors.white), - ), + label: Text(isBlocked ? 'Unblock' : 'Block', + style: const TextStyle(color: Colors.white)), ), ], );