mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-09-04 16:23:22 +00:00
ENH - Apps
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
@@ -66,6 +66,7 @@ button {
|
||||
}
|
||||
.phone-screen {
|
||||
position: relative;
|
||||
container-type: size;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
@@ -207,11 +208,19 @@ button {
|
||||
border-radius: 13px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
color: white;
|
||||
box-shadow:
|
||||
0 4px 13px #0004,
|
||||
inset 0 1px 1px #ffffff55;
|
||||
}
|
||||
.app-icon > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
pointer-events: none;
|
||||
}
|
||||
.app-icon-label {
|
||||
width: 74px;
|
||||
overflow: hidden;
|
||||
@@ -246,6 +255,11 @@ button {
|
||||
.app-icon--settings {
|
||||
background: linear-gradient(145deg, #aeb3bd, #50535b);
|
||||
}
|
||||
.app-icon--image {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
.app-dock {
|
||||
position: absolute;
|
||||
z-index: 8;
|
||||
@@ -978,6 +992,704 @@ button {
|
||||
.wallpaper-picker button.active {
|
||||
border-color: #0a84ff;
|
||||
}
|
||||
|
||||
/* Reference-accurate app surfaces. These remain scoped to the phone resource. */
|
||||
.calculator-app {
|
||||
padding: 54px 16px 27px;
|
||||
}
|
||||
.calculator-display {
|
||||
height: 226px;
|
||||
padding: 0 5px 4px;
|
||||
font-size: 78px;
|
||||
letter-spacing: -5px;
|
||||
}
|
||||
.calculator-pad {
|
||||
gap: 11px 14px;
|
||||
}
|
||||
.calculator-pad button {
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.reference-camera {
|
||||
display: grid;
|
||||
grid-template-rows: 83px minmax(0, 1fr) 190px;
|
||||
padding: 0 0 24px;
|
||||
background: #020202;
|
||||
}
|
||||
.camera-topbar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: end;
|
||||
padding: 45px 15px 11px;
|
||||
}
|
||||
.camera-topbar > div {
|
||||
display: flex;
|
||||
gap: 17px;
|
||||
}
|
||||
.camera-topbar > svg {
|
||||
justify-self: end;
|
||||
}
|
||||
.camera-topbar button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
.camera-topbar .camera-chevron {
|
||||
width: 34px;
|
||||
height: 26px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 20px;
|
||||
background: #262629;
|
||||
}
|
||||
.reference-viewfinder {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 66% 21%, #ffd88baa, transparent 15%),
|
||||
radial-gradient(circle at 45% 52%, #6d8ca5 0 6%, transparent 7%),
|
||||
linear-gradient(150deg, #173542, #5b3e5b 56%, #13191c);
|
||||
}
|
||||
.reference-viewfinder::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
115deg,
|
||||
transparent 45%,
|
||||
#ffffff0f 46%,
|
||||
transparent 48%
|
||||
);
|
||||
}
|
||||
.focus-corner {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-color: white;
|
||||
}
|
||||
.focus-corner--tl {
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
border-top: 1px solid;
|
||||
border-left: 1px solid;
|
||||
}
|
||||
.focus-corner--tr {
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
border-top: 1px solid;
|
||||
border-right: 1px solid;
|
||||
}
|
||||
.focus-corner--bl {
|
||||
bottom: 1px;
|
||||
left: 1px;
|
||||
border-bottom: 1px solid;
|
||||
border-left: 1px solid;
|
||||
}
|
||||
.focus-corner--br {
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
border-right: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
.reference-viewfinder .camera-zoom {
|
||||
z-index: 3;
|
||||
bottom: 13px;
|
||||
}
|
||||
.reference-viewfinder .camera-zoom button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #0008;
|
||||
font-weight: 700;
|
||||
}
|
||||
.reference-viewfinder .camera-zoom small {
|
||||
margin-left: 1px;
|
||||
font-size: 8px;
|
||||
}
|
||||
.reference-camera-footer {
|
||||
display: grid;
|
||||
grid-template-rows: 43px 1fr;
|
||||
background: #020202;
|
||||
}
|
||||
.camera-mode-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 21px;
|
||||
padding: 0 148px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.camera-mode-strip button {
|
||||
flex: none;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: 650;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.camera-mode-strip button.active {
|
||||
color: #ffd60a;
|
||||
}
|
||||
.camera-shutter-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
align-items: center;
|
||||
padding: 0 31px 12px;
|
||||
}
|
||||
.camera-thumbnail {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.reference-shutter {
|
||||
justify-self: center;
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 3px solid white;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
color: #111;
|
||||
box-shadow: inset 0 0 0 3px #020202;
|
||||
}
|
||||
.reference-flip {
|
||||
justify-self: end;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: #1c1c1e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reference-tabbar {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
right: 0;
|
||||
bottom: 21px;
|
||||
left: 0;
|
||||
height: 58px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-around;
|
||||
padding: 7px 5px 0;
|
||||
border-top: 1px solid #ffffff18;
|
||||
background: #101012dd;
|
||||
backdrop-filter: blur(22px);
|
||||
-webkit-backdrop-filter: blur(22px);
|
||||
}
|
||||
.reference-tabbar button {
|
||||
min-width: 49px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #77777c;
|
||||
font-size: 9px;
|
||||
}
|
||||
.reference-tabbar button.active {
|
||||
color: #0a84ff;
|
||||
}
|
||||
.clock-app .app-tabs button.active {
|
||||
color: #d99900;
|
||||
}
|
||||
.clock-app {
|
||||
padding-top: 0;
|
||||
background: #020202;
|
||||
}
|
||||
.reference-clock-header {
|
||||
padding: 47px 16px 0;
|
||||
}
|
||||
.reference-clock-header nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #d99900;
|
||||
}
|
||||
.reference-clock-header nav button {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
font-size: 15px;
|
||||
}
|
||||
.reference-clock-header h1 {
|
||||
margin: 14px 0 0;
|
||||
padding-bottom: 11px;
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
font-size: 35px;
|
||||
line-height: 1;
|
||||
}
|
||||
.clock-app .clock-content {
|
||||
height: calc(100% - 155px);
|
||||
padding: 0 16px 72px;
|
||||
}
|
||||
.world-clock-list > div {
|
||||
min-height: 95px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #ffffff1c;
|
||||
}
|
||||
.world-clock-list > div > div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.world-clock-list small {
|
||||
color: #8e8e93;
|
||||
font-size: 11px;
|
||||
}
|
||||
.world-clock-list strong {
|
||||
font-size: min(25px, 7cqw);
|
||||
font-weight: 400;
|
||||
}
|
||||
.world-clock-list time {
|
||||
font-size: min(42px, 11cqw);
|
||||
font-weight: 200;
|
||||
letter-spacing: -2px;
|
||||
}
|
||||
.reference-clock-tabs button.active {
|
||||
color: #d99900;
|
||||
}
|
||||
|
||||
.reference-photos {
|
||||
padding: 0 0 79px;
|
||||
background: #020202;
|
||||
}
|
||||
.photos-library,
|
||||
.photos-scroll {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.photos-floating-header {
|
||||
position: sticky;
|
||||
z-index: 3;
|
||||
top: 0;
|
||||
height: 95px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
padding: 47px 17px 14px;
|
||||
background: linear-gradient(#000 55%, transparent);
|
||||
}
|
||||
.photos-floating-header > div {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
.photos-floating-header strong {
|
||||
font-size: 17px;
|
||||
}
|
||||
.photos-floating-header span {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.photos-floating-header button {
|
||||
height: 27px;
|
||||
padding: 0 12px;
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
background: #ffffff1d;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
backdrop-filter: blur(15px);
|
||||
-webkit-backdrop-filter: blur(15px);
|
||||
}
|
||||
.photos-floating-header button:last-child {
|
||||
width: 28px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.reference-photo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
.reference-photo-grid article {
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
.photos-count {
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
}
|
||||
.photos-period {
|
||||
position: sticky;
|
||||
bottom: 5px;
|
||||
width: max-content;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
padding: 3px;
|
||||
border-radius: 22px;
|
||||
background: #303033aa;
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
}
|
||||
.photos-period button {
|
||||
padding: 5px 12px;
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
background: transparent;
|
||||
color: #ddd;
|
||||
font-size: 10px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.photos-period button.active {
|
||||
background: #ffffff25;
|
||||
color: white;
|
||||
}
|
||||
.photos-scroll {
|
||||
padding: 82px 16px 20px;
|
||||
}
|
||||
.photos-scroll > h1 {
|
||||
margin: 0 0 28px;
|
||||
font-size: 34px;
|
||||
}
|
||||
.photos-section-title {
|
||||
margin-top: 22px;
|
||||
padding-top: 13px;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
border-top: 1px solid #ffffff1b;
|
||||
}
|
||||
.photos-section-title h2 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
.photos-section-title button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #0a84ff;
|
||||
}
|
||||
.memory-card {
|
||||
position: relative;
|
||||
height: 410px;
|
||||
margin-top: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 17px;
|
||||
box-shadow: inset 0 -160px 100px #0008;
|
||||
}
|
||||
.memory-card > svg {
|
||||
margin-left: auto;
|
||||
}
|
||||
.memory-card > div {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.memory-card strong {
|
||||
font-size: 26px;
|
||||
}
|
||||
.memory-card span {
|
||||
font-size: 11px;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.featured-row {
|
||||
display: flex;
|
||||
gap: 9px;
|
||||
margin-top: 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.featured-row article {
|
||||
width: 88%;
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.featured-row article > div {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.featured-row article span {
|
||||
color: #777;
|
||||
font-size: 12px;
|
||||
}
|
||||
.reference-photos .photo-grid {
|
||||
height: auto;
|
||||
margin-top: 13px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.reference-store {
|
||||
padding: 0 0 79px;
|
||||
background: #0e0e0e;
|
||||
}
|
||||
.store-scroll {
|
||||
height: 100%;
|
||||
padding: 52px 19px 26px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.store-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.store-title > div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 11px;
|
||||
}
|
||||
.store-title h1 {
|
||||
margin: 0;
|
||||
font-size: 34px;
|
||||
}
|
||||
.store-title strong {
|
||||
color: #777;
|
||||
font-size: 18px;
|
||||
}
|
||||
.store-title > span {
|
||||
width: 37px;
|
||||
height: 37px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #3a3c46, #1b1c22);
|
||||
}
|
||||
.editorial-card {
|
||||
position: relative;
|
||||
height: 390px;
|
||||
margin-bottom: 18px;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 10px 28px #0007;
|
||||
}
|
||||
.editorial-card small {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.editorial-card h2 {
|
||||
width: 80%;
|
||||
margin: 7px 0;
|
||||
font-size: 28px;
|
||||
line-height: 1.05;
|
||||
}
|
||||
.editorial-card p {
|
||||
width: 80%;
|
||||
margin: 0;
|
||||
color: #ffffffbf;
|
||||
font-size: 14px;
|
||||
}
|
||||
.editorial-card--sky {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 68% 63%,
|
||||
#fff 0 4%,
|
||||
#4de1fb 5% 9%,
|
||||
transparent 10%
|
||||
),
|
||||
radial-gradient(circle at 65% 60%, #c653ff99, transparent 31%),
|
||||
linear-gradient(145deg, #10144d, #4630a6 58%, #1bc7e2);
|
||||
}
|
||||
.editorial-orbit {
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
bottom: 30px;
|
||||
width: 230px;
|
||||
height: 230px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 2px solid #ffffff99;
|
||||
border-radius: 50%;
|
||||
font-size: 74px;
|
||||
transform: rotate(-18deg);
|
||||
}
|
||||
.editorial-card--music {
|
||||
background:
|
||||
radial-gradient(circle at 60% 70%, #ffdd45, transparent 22%),
|
||||
linear-gradient(155deg, #531849, #ca2c72 60%, #f48c46);
|
||||
}
|
||||
.editorial-wave {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 28px;
|
||||
left: 16px;
|
||||
height: 110px;
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
#ffffffaa 0 4px,
|
||||
transparent 4px 12px
|
||||
);
|
||||
clip-path: polygon(
|
||||
0 45%,
|
||||
10% 10%,
|
||||
20% 82%,
|
||||
30% 25%,
|
||||
40% 70%,
|
||||
50% 5%,
|
||||
60% 85%,
|
||||
70% 28%,
|
||||
80% 68%,
|
||||
90% 13%,
|
||||
100% 55%,
|
||||
100% 70%,
|
||||
0 70%
|
||||
);
|
||||
}
|
||||
.editorial-card--play {
|
||||
height: 330px;
|
||||
background:
|
||||
radial-gradient(circle at 80% 70%, #64ffbe88, transparent 30%),
|
||||
linear-gradient(145deg, #0d4137, #18236c);
|
||||
}
|
||||
.store-section-title {
|
||||
padding: 14px 0 12px;
|
||||
border-top: 1px solid #ffffff1c;
|
||||
}
|
||||
.store-section-title h2 {
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
}
|
||||
.store-section-title p {
|
||||
margin: 3px 0 0;
|
||||
color: #777;
|
||||
font-size: 13px;
|
||||
}
|
||||
.reference-store .store-feature {
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
.reference-store .store-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.reference-settings {
|
||||
padding: 0;
|
||||
background: #0e0e0e;
|
||||
}
|
||||
.settings-scroll {
|
||||
height: 100%;
|
||||
padding: 69px 16px 38px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.settings-scroll > h1 {
|
||||
margin: 0 0 17px;
|
||||
font-size: 35px;
|
||||
line-height: 1;
|
||||
}
|
||||
.settings-search {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 0 9px;
|
||||
border-radius: 9px;
|
||||
background: #1c1c1e;
|
||||
color: #777;
|
||||
}
|
||||
.settings-search input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
}
|
||||
.settings-account {
|
||||
margin: 17px 0;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 12px;
|
||||
background: #1c1c1e;
|
||||
}
|
||||
.settings-account > span {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #596172, #22252c);
|
||||
}
|
||||
.settings-account > div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.settings-account strong {
|
||||
font-size: 17px;
|
||||
}
|
||||
.settings-account small {
|
||||
color: #ccc;
|
||||
font-size: 11px;
|
||||
}
|
||||
.reference-settings-group {
|
||||
margin-bottom: 17px;
|
||||
padding: 0 11px;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background: #1c1c1e;
|
||||
}
|
||||
.reference-settings-group article {
|
||||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.reference-settings-group article:not(:last-child) > div {
|
||||
border-bottom: 1px solid #ffffff18;
|
||||
}
|
||||
.reference-settings-group article > div {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.reference-settings-group article > div strong {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
.reference-settings-group article > div small {
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.reference-settings-group .settings-symbol {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
flex: none;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.settings-chevron {
|
||||
color: #555;
|
||||
}
|
||||
.reference-settings .wallpaper-picker button {
|
||||
height: 105px;
|
||||
}
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
*::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.camera-mode-strip {
|
||||
padding: 0;
|
||||
}
|
||||
.camera-mode-strip button:first-child {
|
||||
margin-left: -205px;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
@@ -18,6 +19,7 @@ const props = withDefaults(
|
||||
|
||||
const phone = usePhoneStore()
|
||||
const router = useRouter()
|
||||
const iconFailed = ref(false)
|
||||
|
||||
function launch(event: MouseEvent): void {
|
||||
const button = event.currentTarget as HTMLElement
|
||||
@@ -49,8 +51,24 @@ function launch(event: MouseEvent): void {
|
||||
:aria-label="phone.t(app.labelKey)"
|
||||
@click="launch"
|
||||
>
|
||||
<span class="app-icon" :class="app.iconClass" aria-hidden="true">
|
||||
<component :is="app.icon" :size="compact ? 18 : 28" :stroke-width="2" />
|
||||
<span
|
||||
class="app-icon"
|
||||
:class="[app.iconClass, { 'app-icon--image': !iconFailed }]"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<img
|
||||
v-if="!iconFailed"
|
||||
:src="app.iconImage"
|
||||
alt=""
|
||||
draggable="false"
|
||||
@error="iconFailed = true"
|
||||
/>
|
||||
<component
|
||||
:is="app.icon"
|
||||
v-else
|
||||
:size="compact ? 18 : 28"
|
||||
:stroke-width="2"
|
||||
/>
|
||||
</span>
|
||||
<span v-if="showLabel" class="app-icon-label">{{
|
||||
phone.t(app.labelKey)
|
||||
|
||||
@@ -8,6 +8,9 @@ describe('app registry', () => {
|
||||
expect(PHONE_APPS.every((app) => app.route === `/apps/${app.id}`)).toBe(
|
||||
true,
|
||||
)
|
||||
expect(PHONE_APPS.every((app) => app.iconImage.endsWith('.webp'))).toBe(
|
||||
true,
|
||||
)
|
||||
expect(
|
||||
PHONE_APPS.filter((app) => app.dockOrder !== null)
|
||||
.sort((a, b) => (a.dockOrder ?? 0) - (b.dockOrder ?? 0))
|
||||
|
||||
@@ -8,6 +8,12 @@ import {
|
||||
} from 'lucide-vue-next'
|
||||
import { defineAsyncComponent, markRaw } from 'vue'
|
||||
|
||||
import appStoreIcon from '@/assets/img/app-icons/apps.webp'
|
||||
import calculatorIcon from '@/assets/img/app-icons/calculator.webp'
|
||||
import cameraIcon from '@/assets/img/app-icons/camera.webp'
|
||||
import clockIcon from '@/assets/img/app-icons/clock.webp'
|
||||
import photosIcon from '@/assets/img/app-icons/gallery.webp'
|
||||
import settingsIcon from '@/assets/img/app-icons/settings.webp'
|
||||
import type { PhoneAppDefinition, PhoneAppId } from '@/types/apps'
|
||||
|
||||
export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
@@ -19,6 +25,7 @@ export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
gridOrder: 0,
|
||||
icon: markRaw(Calculator),
|
||||
iconClass: 'app-icon--calculator',
|
||||
iconImage: calculatorIcon,
|
||||
id: 'calculator',
|
||||
labelKey: 'Apps.calculator.name',
|
||||
route: '/apps/calculator',
|
||||
@@ -31,6 +38,7 @@ export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
gridOrder: 1,
|
||||
icon: markRaw(Camera),
|
||||
iconClass: 'app-icon--camera',
|
||||
iconImage: cameraIcon,
|
||||
id: 'camera',
|
||||
labelKey: 'Apps.camera.name',
|
||||
route: '/apps/camera',
|
||||
@@ -43,6 +51,7 @@ export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
gridOrder: 2,
|
||||
icon: markRaw(Clock3),
|
||||
iconClass: 'app-icon--clock',
|
||||
iconImage: clockIcon,
|
||||
id: 'clock',
|
||||
labelKey: 'Apps.clock.name',
|
||||
route: '/apps/clock',
|
||||
@@ -55,6 +64,7 @@ export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
gridOrder: 3,
|
||||
icon: markRaw(Images),
|
||||
iconClass: 'app-icon--photos',
|
||||
iconImage: photosIcon,
|
||||
id: 'photos',
|
||||
labelKey: 'Apps.photos.name',
|
||||
route: '/apps/photos',
|
||||
@@ -67,6 +77,7 @@ export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
gridOrder: 4,
|
||||
icon: markRaw(ShoppingBag),
|
||||
iconClass: 'app-icon--store',
|
||||
iconImage: appStoreIcon,
|
||||
id: 'app-store',
|
||||
labelKey: 'Apps.appStore.name',
|
||||
route: '/apps/app-store',
|
||||
@@ -79,6 +90,7 @@ export const PHONE_APPS: PhoneAppDefinition[] = [
|
||||
gridOrder: 5,
|
||||
icon: markRaw(Settings),
|
||||
iconClass: 'app-icon--settings',
|
||||
iconImage: settingsIcon,
|
||||
id: 'settings',
|
||||
labelKey: 'Apps.settings.name',
|
||||
route: '/apps/settings',
|
||||
|
||||
@@ -26,6 +26,11 @@ const defaultLocales: LocaleTree = {
|
||||
get: 'GET',
|
||||
open: 'OPEN',
|
||||
searchPlaceholder: 'Games, Apps, Stories and More',
|
||||
communityTitle: 'Black Voices and Creators',
|
||||
communityBody: 'Apps and games from the community',
|
||||
playing: "What We're Playing",
|
||||
recommended: 'Recommended for You',
|
||||
selected: 'Great apps selected by our editors',
|
||||
tabs: {
|
||||
today: 'Today',
|
||||
apps: 'Apps',
|
||||
@@ -39,18 +44,42 @@ const defaultLocales: LocaleTree = {
|
||||
trail: 'Explore nearby',
|
||||
prism: 'A colorful puzzle',
|
||||
},
|
||||
card: {
|
||||
oneEyebrow: 'App of the Day',
|
||||
oneTitle: 'A universe in your pocket',
|
||||
oneBody: 'Explore something extraordinary today.',
|
||||
twoEyebrow: 'Now Trending',
|
||||
twoTitle: 'Turn up your afternoon',
|
||||
twoBody: 'Fresh sounds and stories picked for you.',
|
||||
threeEyebrow: "Editors' Choice",
|
||||
threeTitle: 'Play without limits',
|
||||
threeBody: 'A new world is waiting.',
|
||||
},
|
||||
},
|
||||
calculator: { name: 'Calculator' },
|
||||
camera: {
|
||||
name: 'Camera',
|
||||
shutter: 'Take photo',
|
||||
flip: 'Flip camera',
|
||||
modes: { video: 'Video', photo: 'Photo', portrait: 'Portrait' },
|
||||
flash: 'Toggle flash',
|
||||
controls: 'Camera controls',
|
||||
modes: {
|
||||
timelapse: 'Timelapse',
|
||||
slowMo: 'Slow-Mo',
|
||||
cinematic: 'Cinematic',
|
||||
video: 'Video',
|
||||
photo: 'Photo',
|
||||
portrait: 'Portrait',
|
||||
pano: 'Pano',
|
||||
},
|
||||
},
|
||||
clock: {
|
||||
name: 'Clock',
|
||||
lap: 'Lap',
|
||||
minutes: 'Minutes',
|
||||
add: 'Add clock',
|
||||
today: 'Today',
|
||||
offset: '+0HRS',
|
||||
tabs: {
|
||||
world: 'World Clock',
|
||||
alarm: 'Alarm',
|
||||
@@ -73,6 +102,19 @@ const defaultLocales: LocaleTree = {
|
||||
items: 'items',
|
||||
memories: 'Memories',
|
||||
featured: 'City colors',
|
||||
dateRange: '19 Apr–7 May 2024',
|
||||
place: 'Los Santos & more',
|
||||
select: 'Select',
|
||||
count: '3,042 Photos, 125 Videos',
|
||||
years: 'Years',
|
||||
months: 'Months',
|
||||
days: 'Days',
|
||||
allPhotos: 'All Photos',
|
||||
seeAll: 'See All',
|
||||
onThisDay: 'On This Day',
|
||||
trip: 'MAR 2024 TRIP',
|
||||
featuredPhotos: 'Featured Photos',
|
||||
featuredDate: '30 Mar 2024',
|
||||
tabs: {
|
||||
library: 'Library',
|
||||
forYou: 'For You',
|
||||
@@ -94,6 +136,24 @@ const defaultLocales: LocaleTree = {
|
||||
bluetooth: 'Bluetooth',
|
||||
airplaneMode: 'Airplane Mode',
|
||||
wallpaper: 'Wallpaper',
|
||||
on: 'On',
|
||||
accountName: 'Sky Citizen',
|
||||
accountDetail: 'Apple ID, iCloud, Media & Purchases',
|
||||
mobileServices: 'Mobile Services',
|
||||
personalHotspot: 'Personal Hotspot',
|
||||
vpn: 'VPN',
|
||||
notifications: 'Notifications',
|
||||
sounds: 'Sounds & Haptics',
|
||||
focus: 'Focus',
|
||||
screenTime: 'Screen Time',
|
||||
general: 'General',
|
||||
display: 'Display & Brightness',
|
||||
homeScreen: 'Home Screen & App Library',
|
||||
accessibility: 'Accessibility',
|
||||
battery: 'Battery',
|
||||
privacy: 'Privacy & Security',
|
||||
passwords: 'Passwords',
|
||||
wallpaperPicker: 'Built-in Wallpapers',
|
||||
toggle: {
|
||||
wifi: 'Toggle Wi-Fi',
|
||||
bluetooth: 'Toggle Bluetooth',
|
||||
@@ -109,6 +169,7 @@ const defaultLocales: LocaleTree = {
|
||||
Common: {
|
||||
cancel: 'Cancel',
|
||||
close: 'Close',
|
||||
edit: 'Edit',
|
||||
home: 'Home',
|
||||
pause: 'Pause',
|
||||
phone: 'Phone',
|
||||
|
||||
@@ -23,6 +23,7 @@ export type PhoneAppDefinition = {
|
||||
gridOrder: number
|
||||
icon: Component
|
||||
iconClass: string
|
||||
iconImage: string
|
||||
id: PhoneAppId
|
||||
labelKey: string
|
||||
route: `/apps/${PhoneAppId}`
|
||||
|
||||
@@ -1,37 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Gamepad2,
|
||||
Grid2X2,
|
||||
Rocket,
|
||||
Search,
|
||||
Sparkles,
|
||||
UserRound,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
|
||||
import { useMediaStore } from '@/stores/media'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
|
||||
const phone = usePhoneStore()
|
||||
const media = useMediaStore()
|
||||
const tab = ref('today')
|
||||
const tab = ref<'today' | 'apps' | 'games' | 'arcade' | 'search'>('today')
|
||||
const query = ref('')
|
||||
const tabs = ['today', 'apps', 'games', 'arcade', 'search']
|
||||
const tabs = [
|
||||
{ id: 'today', icon: Sparkles },
|
||||
{ id: 'apps', icon: Grid2X2 },
|
||||
{ id: 'games', icon: Gamepad2 },
|
||||
{ id: 'arcade', icon: Rocket },
|
||||
{ id: 'search', icon: Search },
|
||||
] as const
|
||||
const catalog = [
|
||||
{
|
||||
id: 'orbit',
|
||||
title: 'Orbit',
|
||||
subtitle: 'Apps.appStore.catalog.orbit',
|
||||
gradient: 'linear-gradient(135deg,#6f5cff,#29d3c2)',
|
||||
gradient: 'linear-gradient(145deg,#453bd1,#75e8ff)',
|
||||
},
|
||||
{
|
||||
id: 'studio',
|
||||
title: 'Studio',
|
||||
subtitle: 'Apps.appStore.catalog.studio',
|
||||
gradient: 'linear-gradient(135deg,#ff8b4a,#ff2d75)',
|
||||
gradient: 'linear-gradient(145deg,#ff8b4a,#ff2d75)',
|
||||
},
|
||||
{
|
||||
id: 'trail',
|
||||
title: 'Trail',
|
||||
subtitle: 'Apps.appStore.catalog.trail',
|
||||
gradient: 'linear-gradient(135deg,#4bd37b,#147ec1)',
|
||||
gradient: 'linear-gradient(145deg,#4bd37b,#147ec1)',
|
||||
},
|
||||
{
|
||||
id: 'puzzle',
|
||||
id: 'prism',
|
||||
title: 'Prism',
|
||||
subtitle: 'Apps.appStore.catalog.prism',
|
||||
gradient: 'linear-gradient(135deg,#ffd84d,#7b36dc)',
|
||||
gradient: 'linear-gradient(145deg,#ffd84d,#7b36dc)',
|
||||
},
|
||||
]
|
||||
const shown = computed(() =>
|
||||
@@ -39,53 +54,102 @@ const shown = computed(() =>
|
||||
item.title.toLowerCase().includes(query.value.toLowerCase()),
|
||||
),
|
||||
)
|
||||
const date = new Intl.DateTimeFormat(phone.lang, {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
}).format(new Date())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="native-app store-app">
|
||||
<header class="app-header">
|
||||
<small>{{ phone.t('Apps.appStore.eyebrow') }}</small>
|
||||
<h1>{{ phone.t(`Apps.appStore.tabs.${tab}`) }}</h1>
|
||||
</header>
|
||||
<div v-if="tab === 'search'" class="app-search">
|
||||
<Search :size="17" /><input
|
||||
v-model="query"
|
||||
:placeholder="phone.t('Apps.appStore.searchPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<section class="store-feature">
|
||||
<p>{{ phone.t('Apps.appStore.featured') }}</p>
|
||||
<h2>{{ phone.t('Apps.appStore.heroTitle') }}</h2>
|
||||
<span>{{ phone.t('Apps.appStore.heroBody') }}</span>
|
||||
</section>
|
||||
<section class="store-list">
|
||||
<article v-for="item in shown" :key="item.id">
|
||||
<div class="store-icon" :style="{ background: item.gradient }">
|
||||
{{ item.title[0] }}
|
||||
</div>
|
||||
<main class="native-app reference-store">
|
||||
<section class="store-scroll">
|
||||
<header class="store-title">
|
||||
<div>
|
||||
<strong>{{ item.title }}</strong
|
||||
><small>{{ phone.t(item.subtitle) }}</small>
|
||||
<h1>{{ phone.t(`Apps.appStore.tabs.${tab}`) }}</h1>
|
||||
<strong v-if="tab === 'today'">{{ date }}</strong>
|
||||
</div>
|
||||
<button type="button" @click="media.claimApp(item.id)">
|
||||
{{
|
||||
phone.t(
|
||||
media.claimedApps.includes(item.id)
|
||||
? 'Apps.appStore.open'
|
||||
: 'Apps.appStore.get',
|
||||
)
|
||||
}}
|
||||
</button>
|
||||
</article>
|
||||
<span><UserRound :size="22" /></span>
|
||||
</header>
|
||||
<div v-if="tab === 'search'" class="app-search">
|
||||
<Search :size="17" /><input
|
||||
v-model="query"
|
||||
:placeholder="phone.t('Apps.appStore.searchPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="tab === 'today'">
|
||||
<article class="editorial-card editorial-card--sky">
|
||||
<small>{{ phone.t('Apps.appStore.card.oneEyebrow') }}</small>
|
||||
<h2>{{ phone.t('Apps.appStore.card.oneTitle') }}</h2>
|
||||
<p>{{ phone.t('Apps.appStore.card.oneBody') }}</p>
|
||||
<div class="editorial-orbit">✦</div>
|
||||
</article>
|
||||
<article class="editorial-card editorial-card--music">
|
||||
<small>{{ phone.t('Apps.appStore.card.twoEyebrow') }}</small>
|
||||
<h2>{{ phone.t('Apps.appStore.card.twoTitle') }}</h2>
|
||||
<p>{{ phone.t('Apps.appStore.card.twoBody') }}</p>
|
||||
<div class="editorial-wave" />
|
||||
</article>
|
||||
<div class="store-section-title">
|
||||
<h2>{{ phone.t('Apps.appStore.communityTitle') }}</h2>
|
||||
<p>{{ phone.t('Apps.appStore.communityBody') }}</p>
|
||||
</div>
|
||||
<article class="editorial-card editorial-card--play">
|
||||
<small>{{ phone.t('Apps.appStore.card.threeEyebrow') }}</small>
|
||||
<h2>{{ phone.t('Apps.appStore.card.threeTitle') }}</h2>
|
||||
<p>{{ phone.t('Apps.appStore.card.threeBody') }}</p>
|
||||
</article>
|
||||
</template>
|
||||
<template v-else>
|
||||
<article v-if="tab !== 'search'" class="store-feature">
|
||||
<p>{{ phone.t('Apps.appStore.featured') }}</p>
|
||||
<h2>{{ phone.t('Apps.appStore.heroTitle') }}</h2>
|
||||
<span>{{ phone.t('Apps.appStore.heroBody') }}</span>
|
||||
</article>
|
||||
<div class="store-section-title">
|
||||
<h2>
|
||||
{{
|
||||
phone.t(
|
||||
tab === 'games'
|
||||
? 'Apps.appStore.playing'
|
||||
: 'Apps.appStore.recommended',
|
||||
)
|
||||
}}
|
||||
</h2>
|
||||
<p>{{ phone.t('Apps.appStore.selected') }}</p>
|
||||
</div>
|
||||
<section class="store-list">
|
||||
<article v-for="item in shown" :key="item.id">
|
||||
<div class="store-icon" :style="{ background: item.gradient }">
|
||||
{{ item.title[0] }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ item.title }}</strong
|
||||
><small>{{ phone.t(item.subtitle) }}</small>
|
||||
</div>
|
||||
<button type="button" @click="media.claimApp(item.id)">
|
||||
{{
|
||||
phone.t(
|
||||
media.claimedApps.includes(item.id)
|
||||
? 'Apps.appStore.open'
|
||||
: 'Apps.appStore.get',
|
||||
)
|
||||
}}
|
||||
</button>
|
||||
</article>
|
||||
</section>
|
||||
</template>
|
||||
</section>
|
||||
<nav class="app-tabs">
|
||||
<nav class="reference-tabbar">
|
||||
<button
|
||||
v-for="item in tabs"
|
||||
:key="item"
|
||||
:class="{ active: tab === item }"
|
||||
:key="item.id"
|
||||
:class="{ active: tab === item.id }"
|
||||
type="button"
|
||||
@click="tab = item"
|
||||
@click="tab = item.id"
|
||||
>
|
||||
{{ phone.t(`Apps.appStore.tabs.${item}`) }}
|
||||
<component :is="item.icon" :size="21" /><span>{{
|
||||
phone.t(`Apps.appStore.tabs.${item.id}`)
|
||||
}}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
@@ -1,68 +1,126 @@
|
||||
<script setup lang="ts">
|
||||
import { Camera, RotateCcw } from 'lucide-vue-next'
|
||||
import {
|
||||
Aperture,
|
||||
Camera,
|
||||
ChevronUp,
|
||||
RotateCcw,
|
||||
Zap,
|
||||
ZapOff,
|
||||
} from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useMediaStore } from '@/stores/media'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
|
||||
const media = useMediaStore()
|
||||
const phone = usePhoneStore()
|
||||
const flash = ref(false)
|
||||
const modes = ['video', 'photo', 'portrait'] as const
|
||||
const flashEnabled = ref(false)
|
||||
const modes = [
|
||||
'timelapse',
|
||||
'slowMo',
|
||||
'cinematic',
|
||||
'video',
|
||||
'photo',
|
||||
'portrait',
|
||||
'pano',
|
||||
] as const
|
||||
|
||||
function capture(): void {
|
||||
flash.value = true
|
||||
media.capture()
|
||||
setTimeout(() => {
|
||||
flash.value = false
|
||||
}, 120)
|
||||
window.setTimeout(() => (flash.value = false), 120)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="native-app camera-app">
|
||||
<div
|
||||
class="camera-viewfinder"
|
||||
<main class="native-app reference-camera">
|
||||
<header class="camera-topbar">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.camera.flash')"
|
||||
@click="flashEnabled = !flashEnabled"
|
||||
>
|
||||
<Zap v-if="flashEnabled" :size="22" />
|
||||
<ZapOff v-else :size="22" />
|
||||
</button>
|
||||
<Aperture :size="22" />
|
||||
</div>
|
||||
<button
|
||||
class="camera-chevron"
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.camera.controls')"
|
||||
>
|
||||
<ChevronUp :size="20" />
|
||||
</button>
|
||||
<Aperture :size="22" />
|
||||
</header>
|
||||
|
||||
<section
|
||||
class="reference-viewfinder"
|
||||
:class="`camera-facing--${media.cameraFacing}`"
|
||||
>
|
||||
<div class="camera-flash" :class="{ active: flash }" />
|
||||
<div class="camera-grid" />
|
||||
<i
|
||||
v-for="corner in ['tl', 'tr', 'bl', 'br']"
|
||||
:key="corner"
|
||||
:class="`focus-corner focus-corner--${corner}`"
|
||||
/>
|
||||
<div class="camera-zoom">
|
||||
<button
|
||||
v-for="zoom in [0.5, 1, 2]"
|
||||
v-for="zoom in [0.5, 1]"
|
||||
:key="zoom"
|
||||
:class="{ active: media.cameraZoom === zoom }"
|
||||
type="button"
|
||||
@click="media.cameraZoom = zoom"
|
||||
>
|
||||
{{ zoom }}×
|
||||
{{ zoom }}<small v-if="zoom === 1">x</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="camera-modes">
|
||||
<button
|
||||
v-for="mode in modes"
|
||||
:key="mode"
|
||||
:class="{ active: media.cameraMode === mode }"
|
||||
type="button"
|
||||
@click="media.cameraMode = mode"
|
||||
>
|
||||
{{ phone.t(`Apps.camera.modes.${mode}`) }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="camera-controls">
|
||||
<span /><button
|
||||
class="shutter"
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.camera.shutter')"
|
||||
@click="capture"
|
||||
>
|
||||
<Camera :size="26" /></button
|
||||
><button
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.camera.flip')"
|
||||
@click="
|
||||
media.cameraFacing = media.cameraFacing === 'rear' ? 'front' : 'rear'
|
||||
"
|
||||
>
|
||||
<RotateCcw :size="23" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="reference-camera-footer">
|
||||
<nav class="camera-mode-strip">
|
||||
<button
|
||||
v-for="mode in modes"
|
||||
:key="mode"
|
||||
:class="{ active: media.cameraMode === mode }"
|
||||
type="button"
|
||||
@click="
|
||||
mode === 'photo' || mode === 'portrait' || mode === 'video'
|
||||
? (media.cameraMode = mode)
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
{{ phone.t(`Apps.camera.modes.${mode}`) }}
|
||||
</button>
|
||||
</nav>
|
||||
<div class="camera-shutter-row">
|
||||
<div
|
||||
class="camera-thumbnail"
|
||||
:style="{ background: media.photos[0]?.gradient }"
|
||||
/>
|
||||
<button
|
||||
class="reference-shutter"
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.camera.shutter')"
|
||||
@click="capture"
|
||||
>
|
||||
<Camera :size="23" />
|
||||
</button>
|
||||
<button
|
||||
class="reference-flip"
|
||||
type="button"
|
||||
:aria-label="phone.t('Apps.camera.flip')"
|
||||
@click="
|
||||
media.cameraFacing =
|
||||
media.cameraFacing === 'rear' ? 'front' : 'rear'
|
||||
"
|
||||
>
|
||||
<RotateCcw :size="22" />
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { AlarmClock, Clock3, Plus, Timer, TimerReset } from 'lucide-vue-next'
|
||||
|
||||
import { useClockStore } from '@/stores/clock'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
@@ -35,7 +36,12 @@ const cities = [
|
||||
{ key: 'london', zone: 'Europe/London' },
|
||||
{ key: 'tokyo', zone: 'Asia/Tokyo' },
|
||||
]
|
||||
const tabs = ['world', 'alarm', 'stopwatch', 'timer'] as const
|
||||
const tabs = [
|
||||
{ id: 'world', icon: Clock3 },
|
||||
{ id: 'alarm', icon: AlarmClock },
|
||||
{ id: 'stopwatch', icon: Timer },
|
||||
{ id: 'timer', icon: TimerReset },
|
||||
] as const
|
||||
function cityTime(zone: string): string {
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
hour: '2-digit',
|
||||
@@ -53,14 +59,25 @@ onBeforeUnmount(() => clearInterval(ticker))
|
||||
|
||||
<template>
|
||||
<main class="native-app clock-app">
|
||||
<header class="app-header">
|
||||
<header class="reference-clock-header">
|
||||
<nav>
|
||||
<button type="button">{{ phone.t('Common.edit') }}</button
|
||||
><button type="button" :aria-label="phone.t('Apps.clock.add')">
|
||||
<Plus :size="25" />
|
||||
</button>
|
||||
</nav>
|
||||
<h1>{{ phone.t(`Apps.clock.tabs.${tab}`) }}</h1>
|
||||
</header>
|
||||
<section class="clock-content">
|
||||
<div v-if="tab === 'world'" class="settings-list">
|
||||
<div v-for="city in cities" :key="city.key" class="settings-row">
|
||||
<span>{{ phone.t(`Apps.clock.cities.${city.key}`) }}</span
|
||||
><strong>{{ cityTime(city.zone) }}</strong>
|
||||
<div v-if="tab === 'world'" class="world-clock-list">
|
||||
<div v-for="city in cities" :key="city.key">
|
||||
<div>
|
||||
<small
|
||||
>{{ phone.t('Apps.clock.today') }},
|
||||
{{ phone.t('Apps.clock.offset') }}</small
|
||||
><strong>{{ phone.t(`Apps.clock.cities.${city.key}`) }}</strong>
|
||||
</div>
|
||||
<time>{{ cityTime(city.zone) }}</time>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="tab === 'alarm'" class="settings-list">
|
||||
@@ -162,15 +179,17 @@ onBeforeUnmount(() => clearInterval(ticker))
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<nav class="app-tabs">
|
||||
<nav class="reference-tabbar reference-clock-tabs">
|
||||
<button
|
||||
v-for="item in tabs"
|
||||
:key="item"
|
||||
:class="{ active: tab === item }"
|
||||
:key="item.id"
|
||||
:class="{ active: tab === item.id }"
|
||||
type="button"
|
||||
@click="tab = item"
|
||||
@click="tab = item.id"
|
||||
>
|
||||
{{ phone.t(`Apps.clock.tabs.${item}`) }}
|
||||
<component :is="item.icon" :size="22" /><span>{{
|
||||
phone.t(`Apps.clock.tabs.${item.id}`)
|
||||
}}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
@@ -1,71 +1,134 @@
|
||||
<script setup lang="ts">
|
||||
import { Heart, Images, Library, Search } from 'lucide-vue-next'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Search } from 'lucide-vue-next'
|
||||
|
||||
import { useMediaStore } from '@/stores/media'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
|
||||
const media = useMediaStore()
|
||||
const phone = usePhoneStore()
|
||||
const tab = ref('library')
|
||||
const tab = ref<'library' | 'forYou' | 'albums' | 'search'>('library')
|
||||
const query = ref('')
|
||||
const tabs = ['library', 'forYou', 'albums', 'search']
|
||||
const photos = computed(() =>
|
||||
const tabs = [
|
||||
{ id: 'library', icon: Library },
|
||||
{ id: 'forYou', icon: Heart },
|
||||
{ id: 'albums', icon: Images },
|
||||
{ id: 'search', icon: Search },
|
||||
] as const
|
||||
const gallery = computed(() =>
|
||||
Array.from(
|
||||
{ length: 54 },
|
||||
(_, index) => media.photos[index % media.photos.length],
|
||||
),
|
||||
)
|
||||
const filtered = computed(() =>
|
||||
media.photos.filter((photo) =>
|
||||
phone.t(photo.titleKey).toLowerCase().includes(query.value.toLowerCase()),
|
||||
),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="native-app photos-app">
|
||||
<header class="app-header">
|
||||
<h1>{{ phone.t(`Apps.photos.tabs.${tab}`) }}</h1>
|
||||
</header>
|
||||
<div v-if="tab === 'search'" class="app-search">
|
||||
<Search :size="17" /><input
|
||||
v-model="query"
|
||||
:placeholder="phone.t('Apps.photos.searchPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<section v-if="tab === 'albums'" class="album-cards">
|
||||
<article>
|
||||
<div
|
||||
class="photo-tile"
|
||||
:style="{ background: media.photos[0]?.gradient }"
|
||||
<main class="native-app reference-photos">
|
||||
<section v-if="tab === 'library'" class="photos-library">
|
||||
<header class="photos-floating-header">
|
||||
<div>
|
||||
<strong>{{ phone.t('Apps.photos.dateRange') }}</strong
|
||||
><span>{{ phone.t('Apps.photos.place') }}</span>
|
||||
</div>
|
||||
<button type="button">{{ phone.t('Apps.photos.select') }}</button
|
||||
><button type="button">•••</button>
|
||||
</header>
|
||||
<div class="reference-photo-grid">
|
||||
<article
|
||||
v-for="(photo, index) in gallery"
|
||||
:key="`${photo.id}-${index}`"
|
||||
:style="{ background: photo.gradient }"
|
||||
/>
|
||||
<strong>{{ phone.t('Apps.photos.recents') }}</strong
|
||||
><small
|
||||
>{{ media.photos.length }} {{ phone.t('Apps.photos.items') }}</small
|
||||
>
|
||||
</article>
|
||||
<article>
|
||||
<div class="photo-tile favorites" />
|
||||
<strong>{{ phone.t('Apps.photos.favorites') }}</strong
|
||||
><small>2 {{ phone.t('Apps.photos.items') }}</small>
|
||||
</article>
|
||||
</section>
|
||||
<section v-else-if="tab === 'forYou'" class="featured-photo">
|
||||
<p>{{ phone.t('Apps.photos.memories') }}</p>
|
||||
<div :style="{ background: media.photos[1]?.gradient }">
|
||||
<span>{{ phone.t('Apps.photos.featured') }}</span>
|
||||
</div>
|
||||
<p class="photos-count">{{ phone.t('Apps.photos.count') }}</p>
|
||||
<div class="photos-period">
|
||||
<button>{{ phone.t('Apps.photos.years') }}</button
|
||||
><button>{{ phone.t('Apps.photos.months') }}</button
|
||||
><button>{{ phone.t('Apps.photos.days') }}</button
|
||||
><button class="active">{{ phone.t('Apps.photos.allPhotos') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
<section v-else class="photo-grid">
|
||||
<section v-else-if="tab === 'forYou'" class="photos-scroll">
|
||||
<h1>{{ phone.t('Apps.photos.tabs.forYou') }}</h1>
|
||||
<div class="photos-section-title">
|
||||
<h2>{{ phone.t('Apps.photos.memories') }}</h2>
|
||||
<button>{{ phone.t('Apps.photos.seeAll') }}</button>
|
||||
</div>
|
||||
<article
|
||||
v-for="photo in photos"
|
||||
:key="photo.id"
|
||||
class="photo-tile"
|
||||
:style="{ background: photo.gradient }"
|
||||
:aria-label="phone.t(photo.titleKey)"
|
||||
/>
|
||||
class="memory-card"
|
||||
:style="{ background: media.photos[1]?.gradient }"
|
||||
>
|
||||
<Heart :size="25" />
|
||||
<div>
|
||||
<strong>{{ phone.t('Apps.photos.onThisDay') }}</strong
|
||||
><span>{{ phone.t('Apps.photos.trip') }}</span>
|
||||
</div>
|
||||
</article>
|
||||
<div class="photos-section-title">
|
||||
<h2>{{ phone.t('Apps.photos.featuredPhotos') }}</h2>
|
||||
</div>
|
||||
<div class="featured-row">
|
||||
<article v-for="photo in media.photos.slice(0, 2)" :key="photo.id">
|
||||
<div :style="{ background: photo.gradient }" />
|
||||
<strong>{{ phone.t(photo.titleKey) }}</strong
|
||||
><span>{{ phone.t('Apps.photos.featuredDate') }}</span>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<nav class="app-tabs">
|
||||
<section v-else-if="tab === 'albums'" class="photos-scroll">
|
||||
<h1>{{ phone.t('Apps.photos.tabs.albums') }}</h1>
|
||||
<div class="album-cards">
|
||||
<article>
|
||||
<div
|
||||
class="photo-tile"
|
||||
:style="{ background: media.photos[0]?.gradient }"
|
||||
/>
|
||||
<strong>{{ phone.t('Apps.photos.recents') }}</strong
|
||||
><small
|
||||
>{{ media.photos.length }} {{ phone.t('Apps.photos.items') }}</small
|
||||
>
|
||||
</article>
|
||||
<article>
|
||||
<div class="photo-tile favorites" />
|
||||
<strong>{{ phone.t('Apps.photos.favorites') }}</strong
|
||||
><small>2 {{ phone.t('Apps.photos.items') }}</small>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<section v-else class="photos-scroll">
|
||||
<h1>{{ phone.t('Apps.photos.tabs.search') }}</h1>
|
||||
<div class="app-search">
|
||||
<Search :size="17" /><input
|
||||
v-model="query"
|
||||
:placeholder="phone.t('Apps.photos.searchPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<div class="photo-grid">
|
||||
<article
|
||||
v-for="photo in filtered"
|
||||
:key="photo.id"
|
||||
class="photo-tile"
|
||||
:style="{ background: photo.gradient }"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<nav class="reference-tabbar">
|
||||
<button
|
||||
v-for="item in tabs"
|
||||
:key="item"
|
||||
:class="{ active: tab === item }"
|
||||
:key="item.id"
|
||||
:class="{ active: tab === item.id }"
|
||||
type="button"
|
||||
@click="tab = item"
|
||||
@click="tab = item.id"
|
||||
>
|
||||
{{ phone.t(`Apps.photos.tabs.${item}`) }}
|
||||
<component :is="item.icon" :size="22" /><span>{{
|
||||
phone.t(`Apps.photos.tabs.${item.id}`)
|
||||
}}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
@@ -1,68 +1,158 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Accessibility,
|
||||
BatteryMedium,
|
||||
BellRing,
|
||||
Bluetooth,
|
||||
ChevronRight,
|
||||
Clock3,
|
||||
Focus,
|
||||
Globe2,
|
||||
Grid2X2,
|
||||
KeyRound,
|
||||
LockKeyhole,
|
||||
Monitor,
|
||||
Plane,
|
||||
Radio,
|
||||
Search,
|
||||
Settings,
|
||||
Smartphone,
|
||||
Sun,
|
||||
UserRound,
|
||||
Volume2,
|
||||
Wifi,
|
||||
} from 'lucide-vue-next'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Bluetooth, Search, Wifi, Radio } from 'lucide-vue-next'
|
||||
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import { WALLPAPER_IDS } from '@/utils/preferences'
|
||||
|
||||
const phone = usePhoneStore()
|
||||
const query = ref('')
|
||||
const rows = [
|
||||
{ key: 'wifi', icon: Wifi, color: '#1677ff' },
|
||||
{ key: 'bluetooth', icon: Bluetooth, color: '#1677ff' },
|
||||
{ key: 'airplaneMode', icon: Radio, color: '#ff9500' },
|
||||
] as const
|
||||
const shown = computed(() =>
|
||||
rows.filter((row) =>
|
||||
phone
|
||||
.t(`Apps.settings.${row.key}`)
|
||||
.toLowerCase()
|
||||
.includes(query.value.toLowerCase()),
|
||||
),
|
||||
const groups = [
|
||||
[
|
||||
{ key: 'airplaneMode', icon: Plane, color: '#ff9500', toggle: true },
|
||||
{
|
||||
key: 'wifi',
|
||||
icon: Wifi,
|
||||
color: '#0a84ff',
|
||||
value: 'SkyNet 5G',
|
||||
toggle: true,
|
||||
},
|
||||
{
|
||||
key: 'bluetooth',
|
||||
icon: Bluetooth,
|
||||
color: '#0a84ff',
|
||||
valueKey: 'Apps.settings.on',
|
||||
toggle: true,
|
||||
},
|
||||
{ key: 'mobileServices', icon: Radio, color: '#30d158' },
|
||||
{ key: 'personalHotspot', icon: Smartphone, color: '#30d158' },
|
||||
{ key: 'vpn', icon: Globe2, color: '#0a84ff' },
|
||||
],
|
||||
[
|
||||
{ key: 'notifications', icon: BellRing, color: '#ff453a' },
|
||||
{ key: 'sounds', icon: Volume2, color: '#ff2d55' },
|
||||
{ key: 'focus', icon: Focus, color: '#5e5ce6' },
|
||||
{ key: 'screenTime', icon: Clock3, color: '#5e5ce6' },
|
||||
],
|
||||
[
|
||||
{ key: 'general', icon: Settings, color: '#8e8e93' },
|
||||
{ key: 'display', icon: Sun, color: '#0a84ff' },
|
||||
{ key: 'homeScreen', icon: Grid2X2, color: '#5e5ce6' },
|
||||
{ key: 'accessibility', icon: Accessibility, color: '#0a84ff' },
|
||||
{ key: 'wallpaper', icon: Monitor, color: '#64d2ff' },
|
||||
{ key: 'battery', icon: BatteryMedium, color: '#30d158' },
|
||||
{ key: 'privacy', icon: LockKeyhole, color: '#0a84ff' },
|
||||
],
|
||||
[{ key: 'passwords', icon: KeyRound, color: '#636366' }],
|
||||
]
|
||||
const filteredGroups = computed(() =>
|
||||
groups
|
||||
.map((group) =>
|
||||
group.filter((row) =>
|
||||
phone
|
||||
.t(`Apps.settings.${row.key}`)
|
||||
.toLowerCase()
|
||||
.includes(query.value.toLowerCase()),
|
||||
),
|
||||
)
|
||||
.filter((group) => group.length),
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="native-app settings-app">
|
||||
<header class="app-header">
|
||||
<main class="native-app reference-settings">
|
||||
<section class="settings-scroll">
|
||||
<h1>{{ phone.t('Apps.settings.name') }}</h1>
|
||||
</header>
|
||||
<div class="app-search">
|
||||
<Search :size="17" /><input
|
||||
v-model="query"
|
||||
:placeholder="phone.t('Apps.settings.searchPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<section class="settings-list">
|
||||
<div v-for="row in shown" :key="row.key" class="settings-row">
|
||||
<span class="settings-symbol" :style="{ background: row.color }"
|
||||
><component :is="row.icon" :size="16" /></span
|
||||
><span>{{ phone.t(`Apps.settings.${row.key}`) }}</span
|
||||
><button
|
||||
class="ios-switch"
|
||||
:class="{ active: phone.preferences.settings[row.key] }"
|
||||
type="button"
|
||||
:aria-label="phone.t(`Apps.settings.toggle.${row.key}`)"
|
||||
@click="
|
||||
phone.setSetting(row.key, !phone.preferences.settings[row.key])
|
||||
"
|
||||
>
|
||||
<span />
|
||||
</button>
|
||||
<div class="settings-search">
|
||||
<Search :size="17" /><input
|
||||
v-model="query"
|
||||
:placeholder="phone.t('Apps.settings.searchPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<h2 class="settings-section-title">
|
||||
{{ phone.t('Apps.settings.wallpaper') }}
|
||||
</h2>
|
||||
<section class="wallpaper-picker">
|
||||
<button
|
||||
v-for="wallpaper in WALLPAPER_IDS"
|
||||
:key="wallpaper"
|
||||
type="button"
|
||||
:class="[
|
||||
`wallpaper--${wallpaper}`,
|
||||
{ active: phone.preferences.settings.wallpaper === wallpaper },
|
||||
]"
|
||||
:aria-label="phone.t(`Apps.settings.wallpapers.${wallpaper}`)"
|
||||
@click="phone.setWallpaper(wallpaper)"
|
||||
/>
|
||||
<article class="settings-account">
|
||||
<span><UserRound :size="34" /></span>
|
||||
<div>
|
||||
<strong>{{ phone.t('Apps.settings.accountName') }}</strong
|
||||
><small>{{ phone.t('Apps.settings.accountDetail') }}</small>
|
||||
</div>
|
||||
<ChevronRight :size="19" />
|
||||
</article>
|
||||
<section
|
||||
v-for="(group, groupIndex) in filteredGroups"
|
||||
:key="groupIndex"
|
||||
class="reference-settings-group"
|
||||
>
|
||||
<article v-for="row in group" :key="row.key">
|
||||
<span class="settings-symbol" :style="{ background: row.color }"
|
||||
><component :is="row.icon" :size="16"
|
||||
/></span>
|
||||
<div>
|
||||
<strong>{{ phone.t(`Apps.settings.${row.key}`) }}</strong
|
||||
><small v-if="row.value">{{ row.value }}</small
|
||||
><small v-else-if="row.valueKey">{{ phone.t(row.valueKey) }}</small>
|
||||
</div>
|
||||
<button
|
||||
v-if="row.toggle"
|
||||
class="ios-switch"
|
||||
:class="{
|
||||
active:
|
||||
phone.preferences.settings[
|
||||
row.key as 'airplaneMode' | 'wifi' | 'bluetooth'
|
||||
],
|
||||
}"
|
||||
type="button"
|
||||
:aria-label="phone.t(`Apps.settings.toggle.${row.key}`)"
|
||||
@click="
|
||||
phone.setSetting(
|
||||
row.key as 'airplaneMode' | 'wifi' | 'bluetooth',
|
||||
!phone.preferences.settings[
|
||||
row.key as 'airplaneMode' | 'wifi' | 'bluetooth'
|
||||
],
|
||||
)
|
||||
"
|
||||
>
|
||||
<span /></button
|
||||
><ChevronRight v-else :size="18" class="settings-chevron" />
|
||||
</article>
|
||||
</section>
|
||||
<h2 class="settings-section-title">
|
||||
{{ phone.t('Apps.settings.wallpaperPicker') }}
|
||||
</h2>
|
||||
<section class="wallpaper-picker">
|
||||
<button
|
||||
v-for="wallpaper in WALLPAPER_IDS"
|
||||
:key="wallpaper"
|
||||
type="button"
|
||||
:class="[
|
||||
`wallpaper--${wallpaper}`,
|
||||
{ active: phone.preferences.settings.wallpaper === wallpaper },
|
||||
]"
|
||||
:aria-label="phone.t(`Apps.settings.wallpapers.${wallpaper}`)"
|
||||
@click="phone.setWallpaper(wallpaper)"
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user