27 Commits
Author SHA1 Message Date
gawells 2ba179a62b make things look nicer and work on image change 2026-06-30 14:24:03 -04:00
gawells d2b0955303 calculate cropping boxes for the image 2026-06-30 14:16:32 -04:00
gawells 1426164dc4 stop annoying drag on some handles 2026-06-30 12:53:02 -04:00
gawells 16cf675e87 fix images that are too small from being rendered 2026-06-30 12:51:50 -04:00
gawells 8685cb4e6f resize image to fit bounds 2026-06-30 12:46:11 -04:00
gawells 0b47af3b4f stop grabbing of ghost image 2026-06-26 16:03:18 -04:00
gawells 38e8ebf362 resize the box when anchors are resized 2026-06-26 16:00:22 -04:00
gawells 1aaf6825bd reset position on mouse up 2026-06-23 11:41:26 -04:00
gawells 438c1ce9ef fix grabbing and moving 2026-06-22 20:01:28 -04:00
gawells 04b01dbf27 move anchors around 2026-06-22 17:42:09 -04:00
gawells 68d4baee61 add offsets into handles 2026-06-14 17:39:38 -04:00
gawells f04a9956ff detect which handle is being held 2026-06-14 16:55:04 -04:00
gawells 6a0df8a49a change cursor for anchors 2026-06-14 09:46:55 -04:00
gawells 791dd3b0c4 place resize anchors 2026-06-14 08:52:59 -04:00
gawells 3d6a853be0 Update ReadME.md 2026-06-14 08:17:18 -04:00
gawells 7d1be226b6 move some stuff to a new JS file 2026-06-14 08:13:30 -04:00
gawells 65ed53c5d1 grab picture when moving it around 2026-06-14 07:58:29 -04:00
gawells ff5d9724d6 simple cursor controller 2026-06-14 07:58:17 -04:00
gawells 8859449be5 add boundies for moving image around 2026-06-13 13:41:39 -04:00
gawells f0ace2b43f unblur background on error 2026-06-12 13:23:50 -04:00
gawells 07f78510ed display profile picture image 2026-06-12 13:16:14 -04:00
gawells 4870318148 add a cursor helper 2026-06-11 18:58:57 -04:00
gawells c9f9f3ef6c add in confirm change button 2026-06-11 18:38:37 -04:00
gawells d82d2bba20 redo in memory sesisons to have TTLs 2026-06-11 18:13:31 -04:00
gawells 22c4666fa7 update session stores to decide TTLs 2026-06-09 12:31:27 -04:00
gawells e41a81c487 get a cropped square to exist 2026-06-08 21:36:10 -04:00
gawells 600c3abf20 make new photo pop up in box before changing 2026-06-08 21:09:12 -04:00
13 changed files with 567 additions and 67 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ A simple, lightweight web application for managing user profile photos in a Free
## Prerequisites ## Prerequisites
* **Go 1.26+** installed on your machine. * **Go 1.26+** installed on your machine.
* Access to a **FreeIPA Server**. * Access to a **FreeIPA Server**.
* A Service Account with permission to search and modify the `jpegPhoto` attribute. * A Service Account with permission to search and modify the `jpegPhoto` attribute and read the `krbPasswordExpiration` attribute.
## Setup & Installation ## Setup & Installation
+120 -16
View File
@@ -10,6 +10,7 @@
<link rel="stylesheet" href="static/error/error.css" /> <link rel="stylesheet" href="static/error/error.css" />
<link rel="stylesheet" href="static/profile_page.css" /> <link rel="stylesheet" href="static/profile_page.css" />
<link rel="stylesheet" href="static/progress_bar.css" /> <link rel="stylesheet" href="static/progress_bar.css" />
<link rel="stylesheet" href="static/cursor.css" />
<div id="popup_background" class="blocked hidden"></div> <div id="popup_background" class="blocked hidden"></div>
@@ -87,20 +88,53 @@
></div> ></div>
</div> </div>
</div> </div>
<!--<div id="password_errors">
<div class="password_error">Error 1</div>
<div class="password_error">Error 2</div>
</div>
<div id="password_warnings">
<div class="password_error">Error 1</div>
<div class="password_error">Error 2</div>
</div>-->
<button id="final_change_password_button">Change Password</button> <button id="final_change_password_button">Change Password</button>
</div> </div>
<div
id="resize_photo_dialouge"
class="card static_center hidden"
draggable="false"
>
<div id="image_container" draggable="false">
<img
id="new_profile_image"
alt="new-profile-image"
class="noselect"
draggable="false"
/>
<div id="image_dark_overlay" draggable="false"></div>
</div>
<div id="grab-area" draggable="false">
<div id="box-scale-holder">
<div
draggable="false"
id="top_left_resize"
class="resize_anchor"
></div>
<div
draggable="false"
id="top_right_resize"
class="resize_anchor"
></div>
<div
draggable="false"
id="bottom_left_resize"
class="resize_anchor"
></div>
<div
draggable="false"
id="bottom_right_resize"
class="resize_anchor"
></div>
</div>
<div id="resize_photo_box" draggable="false">
<div id="circle-display"></div>
</div>
</div>
<button id="confirm_change" draggable="false">Confirm Change</button>
</div>
<img id="logo_image" alt="logo" src="/logo" /> <img id="logo_image" alt="logo" src="/logo" />
<div id="main_content"> <div id="main_content">
<div class="cards"> <div class="cards">
@@ -168,28 +202,98 @@
</div> </div>
</div> </div>
<script src="/static/javascript/helpers.js" type="text/javascript"></script>
<script src="/static/javascript/cursor.js" type="text/javascript"></script>
<script
src="/static/javascript/resize_photo_dialouge.js"
type="text/javascript"
></script>
<script type="text/javascript"> <script type="text/javascript">
const button = document.getElementById("edit_picture_button"); const button = document.getElementById("edit_picture_button");
const fileInput = document.getElementById("file_input"); const fileInput = document.getElementById("file_input");
const confirm_change = document.getElementById("confirm_change");
const new_profile_image = document.getElementById("new_profile_image");
button.addEventListener("click", () => { button.addEventListener("click", () => {
fileInput.click(); // opens file picker fileInput.click();
}); });
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
var currentPreviewURL = null; var currentPreviewURL = null,
image = null,
file = null;
var x_start = 0,
y_start = 0;
var image_width = 0,
image_height = 0;
fileInput.addEventListener("change", async () => { fileInput.addEventListener("change", async () => {
document.getElementById("popup_background").classList.remove("hidden"); document.getElementById("popup_background").classList.remove("hidden");
const file = fileInput.files[0]; file = fileInput.files[0];
if (!file) return; if (!file) return;
if (file.type !== "image/jpeg") { if (file.type !== "image/jpeg") {
alert("Only JPEG images are allowed."); alert("Only JPEG images are allowed.");
fileInput.value = ""; fileInput.value = "";
document.getElementById("popup_background").classList.add("hidden");
return; return;
} }
document
.getElementById("resize_photo_dialouge")
.classList.remove("hidden");
image = URL.createObjectURL(file);
var bitmap = await createImageBitmap(file);
new_profile_image.src = image;
new_profile_image.style.setProperty("--x-offset", "0px");
new_profile_image.style.setProperty("--y-offset", "0px");
smaller_dimension = min(bitmap.width, bitmap.height);
diff_to_350 = 360 / smaller_dimension;
image_width = bitmap.width * diff_to_350;
image_height = bitmap.height * diff_to_350;
new_profile_image.style.setProperty("--x-size", image_width + "px");
new_profile_image.style.setProperty("--y-size", image_height + "px");
image_dark_overlay.style.setProperty("--x-size", image_width + "px");
image_dark_overlay.style.setProperty("--y-size", image_height + "px");
image_dark_overlay.style.setProperty("--x-offset", "0px");
image_dark_overlay.style.setProperty("--y-offset", "0px");
bounds = calculateBoxBoundsOnImage({
x: 0,
y: 0,
});
image_dark_overlay.style.setProperty("--left-edge", bounds.left + "px");
image_dark_overlay.style.setProperty(
"--right-edge",
bounds.right + "px",
);
image_dark_overlay.style.setProperty("--top-edge", bounds.top + "px");
image_dark_overlay.style.setProperty(
"--bottom-edge",
bounds.bottom + "px",
);
resize_photo_box.style.setProperty("--size", "350px");
});
confirm_change.addEventListener("click", async () => {
document
.getElementById("resize_photo_dialouge")
.classList.add("hidden");
const formData = new FormData(); const formData = new FormData();
formData.append("photo", file); formData.append("photo", file);
formData.append( formData.append(
@@ -211,8 +315,8 @@
URL.revokeObjectURL(currentPreviewURL); URL.revokeObjectURL(currentPreviewURL);
} }
img.src = URL.createObjectURL(file); img.src = image;
currentPreviewURL = img.src; currentPreviewURL = image;
}); });
</script> </script>
+1 -23
View File
@@ -7,7 +7,6 @@ import (
"astraltech.xyz/accountmanager/src/logging" "astraltech.xyz/accountmanager/src/logging"
"astraltech.xyz/accountmanager/src/store" "astraltech.xyz/accountmanager/src/store"
"astraltech.xyz/accountmanager/src/worker"
) )
const SessionCookieName = "session_token" const SessionCookieName = "session_token"
@@ -39,10 +38,6 @@ func (manager *SessionManager) SetStoreType(storeType StoreType, params ...any)
case InMemory: case InMemory:
{ {
manager.store = store.NewMemoryStore[*SessionData]() manager.store = store.NewMemoryStore[*SessionData]()
worker.CreateWorker(time.Minute*5, func() {
inMemStore, _ := manager.store.(*store.MemoryStore[*SessionData])
cleanupInMemoryStore(inMemStore)
})
break break
} }
case Redis: case Redis:
@@ -70,7 +65,7 @@ func (manager *SessionManager) CreateSession(userID string) (cookie *http.Cookie
CSRFToken: CSRFToken, CSRFToken: CSRFToken,
ExpiresAt: time.Now().Add(time.Hour), ExpiresAt: time.Now().Add(time.Hour),
} }
err = manager.store.Create(token, &newSessionData) err = manager.store.CreateExpiring(token, &newSessionData, time.Hour)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -110,23 +105,6 @@ func (manager *SessionManager) GetSession(r *http.Request) (*SessionData, error)
return data, nil return data, nil
} }
func cleanupInMemoryStore(m *store.MemoryStore[*SessionData]) {
logging.Debug("Cleaning up memory store sessions")
now := time.Now()
m.Lock.Lock()
defer m.Lock.Unlock()
deleted := 0
for id, session := range m.Sessions {
if now.After(session.ExpiresAt) {
delete(m.Sessions, id)
deleted = deleted + 1
}
}
logging.Infof("Cleaned up %d stale sessions", deleted)
}
func (manager *SessionManager) DeleteSession(sessionId string) error { func (manager *SessionManager) DeleteSession(sessionId string) error {
return manager.store.Delete(sessionId) return manager.store.Delete(sessionId)
} }
+5
View File
@@ -1,9 +1,14 @@
package store package store
import (
"time"
)
// A simple key value store that can either just be single instance in memory or a redis server (for now) // A simple key value store that can either just be single instance in memory or a redis server (for now)
type KeyValueStore[Value any] interface { type KeyValueStore[Value any] interface {
Create(key string, value Value) error Create(key string, value Value) error
CreateExpiring(key string, value Value, ttl time.Duration) error
Get(key string) (Value, error) Get(key string) (Value, error)
Update(key string, session Value) error Update(key string, session Value) error
Delete(key string) error Delete(key string) error
+108 -23
View File
@@ -2,72 +2,157 @@ package store
import ( import (
"sync" "sync"
"time"
"astraltech.xyz/accountmanager/src/logging" "astraltech.xyz/accountmanager/src/logging"
"astraltech.xyz/accountmanager/src/worker"
) )
type KeyValue[Value any] struct {
value Value
expireTime time.Time
}
type ExpireTime struct {
expiryTime time.Time
key string
}
type MemoryStore[Value any] struct { type MemoryStore[Value any] struct {
Sessions map[string]Value Keys map[string]KeyValue[Value]
Lock sync.RWMutex Lock sync.RWMutex
ExpireTimes []ExpireTime
} }
func NewMemoryStore[Value any]() *MemoryStore[Value] { func NewMemoryStore[Value any]() *MemoryStore[Value] {
logging.Debug("Creating new in memory session store") logging.Debug("Creating new in memory session store")
store := &MemoryStore[Value]{ store := &MemoryStore[Value]{
Sessions: make(map[string]Value), Keys: make(map[string]KeyValue[Value]),
Lock: sync.RWMutex{},
ExpireTimes: []ExpireTime{},
} }
worker.CreateWorker(time.Minute*5, store.CleanupExpired)
return store return store
} }
func (m *MemoryStore[Value]) Create(key string, session Value) (err error) { func (m *MemoryStore[Value]) CreateExpiring(key string, value Value, duration time.Duration) error {
hashedkey := HashKey(key) var expiry time.Time
if duration != 0 {
expiry = time.Now().Add(duration)
}
m.Lock.Lock() m.Lock.Lock()
defer m.Lock.Unlock() defer m.Lock.Unlock()
_, exist := m.Sessions[hashedkey]
hashedkey := HashKey(key)
_, exist := m.Keys[hashedkey]
if exist { if exist {
return ErrKeyAlreadyExists return ErrKeyAlreadyExists
} }
m.Sessions[hashedkey] = session m.Keys[hashedkey] = KeyValue[Value]{
value: value,
expireTime: expiry,
}
if duration != 0 {
low := 0
high := len(m.ExpireTimes)
for low < high {
mid := (low + high) / 2
if m.ExpireTimes[mid].expiryTime.Before(expiry) {
low = mid + 1
} else {
high = mid
}
}
insertIndex := low
m.ExpireTimes = append(m.ExpireTimes, ExpireTime{})
copy(m.ExpireTimes[insertIndex+1:], m.ExpireTimes[insertIndex:])
m.ExpireTimes[insertIndex] = ExpireTime{
key: key,
expiryTime: expiry,
}
}
return nil return nil
} }
func (m *MemoryStore[Value]) CleanupExpired() {
m.Lock.Lock()
defer m.Lock.Unlock()
now := time.Now()
for len(m.ExpireTimes) > 0 && !now.Before(m.ExpireTimes[0].expiryTime) {
key, exists := m.Keys[HashKey(m.ExpireTimes[0].key)]
if exists && key.expireTime.Equal(m.ExpireTimes[0].expiryTime) {
m.deleteWithoutLock(m.ExpireTimes[0].key)
}
m.ExpireTimes = m.ExpireTimes[1:]
}
}
func (m *MemoryStore[Value]) Create(key string, session Value) error {
return m.CreateExpiring(key, session, 0)
}
func (m *MemoryStore[Value]) Get(key string) (Value, error) { func (m *MemoryStore[Value]) Get(key string) (Value, error) {
var data Value var data KeyValue[Value]
var zeroValue Value
hashedkey := HashKey(key)
m.Lock.RLock() m.Lock.RLock()
hashedkey := HashKey(key) data, exists := m.Keys[hashedkey]
data, exists := m.Sessions[hashedkey]
m.Lock.RUnlock() m.Lock.RUnlock()
if exists == false {
return data, ErrKeyNotFound if !exists {
return zeroValue, ErrKeyNotFound
} }
return data, nil
if !data.expireTime.IsZero() && !time.Now().Before(data.expireTime) {
m.Lock.Lock()
if current, ok := m.Keys[hashedkey]; ok && current.expireTime.Equal(data.expireTime) {
delete(m.Keys, hashedkey)
}
m.Lock.Unlock()
return zeroValue, ErrKeyNotFound
}
return data.value, nil
} }
func (m *MemoryStore[Value]) Update(sessionID string, session Value) error { func (m *MemoryStore[Value]) Update(sessionID string, value Value) error {
hashedkey := HashKey(sessionID) hashedkey := HashKey(sessionID)
m.Lock.Lock() m.Lock.Lock()
defer m.Lock.Unlock() defer m.Lock.Unlock()
_, exist := m.Sessions[hashedkey] old_key, exist := m.Keys[hashedkey]
if !exist { if !exist {
return ErrKeyNotFound return ErrKeyNotFound
} }
m.Sessions[hashedkey] = session m.Keys[hashedkey] = KeyValue[Value]{
value: value,
expireTime: old_key.expireTime,
}
return nil return nil
} }
func (m *MemoryStore[Value]) Delete(sessionID string) error { func (m *MemoryStore[Value]) deleteWithoutLock(key string) error {
hashedkey := HashKey(sessionID) hashedkey := HashKey(key)
_, exist := m.Keys[hashedkey]
if !exist {
return ErrKeyNotFound
}
delete(m.Keys, hashedkey)
return nil
}
func (m *MemoryStore[Value]) Delete(key string) error {
m.Lock.Lock() m.Lock.Lock()
defer m.Lock.Unlock() defer m.Lock.Unlock()
_, exist := m.Sessions[hashedkey] return m.deleteWithoutLock(key)
if !exist {
return ErrKeyNotFound
}
delete(m.Sessions, hashedkey)
return nil
} }
+5 -3
View File
@@ -43,8 +43,7 @@ func NewRedisStore[Value any](redis_server string, prefix string) *RedisStore[Va
} }
return store return store
} }
func (m *RedisStore[Value]) CreateExpiring(key string, value Value, ttl time.Duration) error {
func (m *RedisStore[Value]) Create(key string, value Value) (err error) {
hashedSession := m.RedisHash(key) hashedSession := m.RedisHash(key)
data, err := json.Marshal(value) data, err := json.Marshal(value)
@@ -52,7 +51,7 @@ func (m *RedisStore[Value]) Create(key string, value Value) (err error) {
return ErrKeyBackend return ErrKeyBackend
} }
created, err := m.client.SetNX(m.ctx, hashedSession, data, time.Hour).Result() created, err := m.client.SetNX(m.ctx, hashedSession, data, ttl).Result()
if err != nil { if err != nil {
logging.Error(err.Error()) logging.Error(err.Error())
return ErrKeyBackend return ErrKeyBackend
@@ -63,6 +62,9 @@ func (m *RedisStore[Value]) Create(key string, value Value) (err error) {
} }
return nil return nil
} }
func (m *RedisStore[Value]) Create(key string, value Value) error {
return m.CreateExpiring(key, value, 0)
}
func (m *RedisStore[Value]) Get(sessionID string) (Value, error) { func (m *RedisStore[Value]) Get(sessionID string) (Value, error) {
hashed := m.RedisHash(sessionID) hashed := m.RedisHash(sessionID)
var session_data Value var session_data Value
+1
View File
@@ -9,6 +9,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; gap: 15px;
overflow: hidden;
} }
.static_center { .static_center {
+3
View File
@@ -0,0 +1,3 @@
.cursor_grabbing {
cursor: grabbing;
}
+20
View File
@@ -0,0 +1,20 @@
var currentX, currentY;
document.addEventListener("mousemove", (e) => {
const rect = document.body.getBoundingClientRect();
currentX = e.clientX;
currentY = e.clientY;
});
const CURSOR_NORMAL = "cursor_normal";
const CURSOR_GRABBING = "cursor_grabbing";
document.body.classList.remove(CURSOR_NORMAL);
let current_cursor_state = CURSOR_NORMAL;
function set_cursor_state(new_cursor_state) {
document.body.classList.remove(current_cursor_state);
document.body.classList.add(new_cursor_state);
current_cursor_state = new_cursor_state;
}
+13
View File
@@ -0,0 +1,13 @@
function min(a, b) {
if (a < b) {
return a;
}
return b;
}
function max(a, b) {
if (a > b) {
return a;
}
return b;
}
+134
View File
@@ -0,0 +1,134 @@
const resize_photo_box = document.getElementById("resize_photo_box");
const image_dark_overlay = document.getElementById("image_dark_overlay");
// Drag handling
var startXOffset = 0,
startYOffset = 0;
function calculateOffsets() {
const yOffset = startYOffset + (currentY - y_start);
const xOffset = startXOffset + (currentX - x_start);
var top = image_height / 2 - yOffset - 175 - 5;
if (top > 0) top = 0;
var bottom = -(image_height / 2) - yOffset + 5 + 175;
if (bottom < 0) bottom = 0;
var left = image_width / 2 - xOffset - 175 - 5;
if (left > 0) left = 0;
var right = -(image_width / 2) - xOffset + 5 + 175;
if (right < 0) right = 0;
return { x: xOffset + left + right, y: yOffset + top + bottom };
}
function calculateBoxBoundsOnImage(offsets) {
const x_overspill = image_width - 360;
const left_overspill = x_overspill / 2 - offsets.x;
const y_overspill = image_height - 360;
const top_overspill = y_overspill / 2 - offsets.y;
return {
left: left_overspill,
right: left_overspill + 360,
top: top_overspill,
bottom: top_overspill + 360,
};
}
var mouseClicked = false;
resize_photo_box.addEventListener("mousemove", () => {
if (!mouseClicked) return;
offsets = calculateOffsets();
new_profile_image.style.setProperty("--x-offset", offsets.x + "px");
new_profile_image.style.setProperty("--y-offset", offsets.y + "px");
image_dark_overlay.style.setProperty("--x-offset", offsets.x + "px");
image_dark_overlay.style.setProperty("--y-offset", offsets.y + "px");
bounds = calculateBoxBoundsOnImage(offsets);
image_dark_overlay.style.setProperty("--left-edge", bounds.left + "px");
image_dark_overlay.style.setProperty("--right-edge", bounds.right + "px");
image_dark_overlay.style.setProperty("--top-edge", bounds.top + "px");
image_dark_overlay.style.setProperty("--bottom-edge", bounds.bottom + "px");
});
resize_photo_box.addEventListener("mousedown", () => {
x_start = currentX;
y_start = currentY;
mouseClicked = true;
set_cursor_state(CURSOR_GRABBING);
});
let unclickedMouse = false;
resize_photo_box.addEventListener("mouseup", () => {
if (!unclickedMouse) return;
unclickedMouse = false;
offsets = calculateOffsets();
startXOffset = offsets.x;
startYOffset = offsets.y;
});
document.body.addEventListener("mouseup", () => {
if (mouseClicked) unclickedMouse = true;
mouseClicked = false;
set_cursor_state(CURSOR_NORMAL);
});
// handle anchors
resize_anchors = document.getElementsByClassName("resize_anchor");
var handleBeingHeld = [false, false, false, false];
var startX, startY;
for (var i = 0; i < resize_anchors.length; i++) {
const index = i;
resize_anchors[index].addEventListener("mousedown", (e) => {
e.stopPropagation();
handleBeingHeld[index] = true;
startX = currentX;
startY = currentY;
});
document.body.addEventListener("mouseup", () => {
handleBeingHeld[index] = false;
resize_anchors[index].style.setProperty("--x-offset", "0px");
resize_anchors[index].style.setProperty("--y-offset", "0px");
resize_photo_box.style.setProperty("--size", "350px");
resize_photo_box.style.setProperty("--box-x-offset", "0px");
resize_photo_box.style.setProperty("--box-y-offset", "0px");
});
resize_anchors[i].style.setProperty("--x-offset", "0px");
resize_anchors[i].style.setProperty("--y-offset", "0px");
}
document.body.addEventListener("mousemove", () => {
for (var i = 0; i < handleBeingHeld.length; i++) {
if (!handleBeingHeld[i]) continue;
newX = Math.abs(startX - currentX);
newY = Math.abs(currentY - startY);
moveAmount = max(min(newX, newY), 0);
resize_anchors[i].style.setProperty("--x-offset", moveAmount + "px");
resize_anchors[i].style.setProperty("--y-offset", moveAmount + "px");
resize_photo_box.style.setProperty("--size", 350 - moveAmount + "px");
if (i == 0) {
resize_photo_box.style.setProperty("--box-x-offset", moveAmount + "px");
resize_photo_box.style.setProperty("--box-y-offset", moveAmount + "px");
}
if (i == 1) {
resize_photo_box.style.setProperty("--box-y-offset", moveAmount + "px");
}
if (i == 2) {
resize_photo_box.style.setProperty("--box-x-offset", moveAmount + "px");
}
}
});
+146
View File
@@ -116,6 +116,10 @@
position: fixed; position: fixed;
} }
#resize_photo_dialouge {
z-index: 10000;
}
#change_password_dialogue input { #change_password_dialogue input {
width: 350px; width: 350px;
} }
@@ -149,3 +153,145 @@
border-style: solid; border-style: solid;
color: var(--text-main); color: var(--text-main);
} }
#image_container {
position: absolute;
display: inline-block;
}
#darken_part {
/*background-color: black;*/
z-index: 3;
width: 100vw;
height: 100vh;
}
#confirm_change {
z-index: 2;
}
#new_profile_image {
z-index: 1;
transform: translateX(calc(-50% + var(--x-offset) + 5px))
translateY(calc(-50% + var(--y-offset) + 5px));
position: absolute;
left: calc(350px / 2);
top: calc(350px / 2);
width: var(--x-size);
height: var(--y-size);
image-rendering: pixelated;
}
#grab-area {
display: grid;
}
#grab-area > * {
grid-column-start: 1;
grid-row-start: 1;
}
#resize_photo_box {
background-color: rgba(0, 0, 0, 0);
width: var(--size);
aspect-ratio: 1 / 1;
border-style: dashed;
border-color: black;
border-width: 5px;
z-index: 4;
transform: translateX(var(--box-x-offset)) translateY(var(--box-y-offset));
}
.resize_anchor {
background-color: red;
width: 20px;
height: 20px;
position: absolute;
z-index: 5;
}
#top_left_resize {
transform: translateX(calc(-50% + var(--x-offset) + 10px))
translateY(calc(-50% + var(--y-offset) + 10px));
}
#top_left_resize:hover {
cursor: nw-resize !important;
}
#top_right_resize {
transform: translateX(calc(350px - var(--x-offset) - 10px))
translateY(calc(-50% + var(--y-offset) + 10px));
}
#top_right_resize:hover {
cursor: ne-resize !important;
}
#bottom_left_resize {
transform: translateX(calc(-50% + var(--x-offset) + 10px))
translateY(calc(340px - var(--x-offset)));
}
#bottom_left_resize:hover {
cursor: sw-resize !important;
}
#bottom_right_resize {
transform: translateX(calc(340px - var(--x-offset)))
translateY(calc(340px - var(--y-offset)));
}
#bottom_right_resize:hover {
cursor: se-resize !important;
}
#box-scale-holder {
position: relative;
width: 360px;
height: 360px;
}
#circle-display {
position: absolute;
width: calc(100% + 10px);
height: calc(100% + 10px);
transform: translateX(-5px) translateY(-5px);
transform: scale(10px);
background: radial-gradient(
circle closest-side,
transparent 99%,
rgba(0, 0, 0, 0.5) 100%
);
/*dispalt*/
}
#image_dark_overlay {
background-color: rgba(0, 0, 0, 0.5);
pointer-events: none;
z-index: 2;
transform: translateX(calc(-50% + var(--x-offset) + 5px))
translateY(calc(-50% + var(--y-offset) + 5px));
position: absolute;
left: calc(350px / 2);
top: calc(350px / 2);
width: var(--x-size);
height: var(--y-size);
clip-path: polygon(
0% 0%,
0% 100%,
var(--left-edge) 100%,
var(--left-edge) var(--top-edge),
var(--right-edge) var(--top-edge),
var(--right-edge) var(--bottom-edge),
var(--left-edge) var(--bottom-edge),
var(--left-edge) 100%,
100% 100%,
100% 0%
);
}
+9
View File
@@ -140,3 +140,12 @@ input::placeholder {
.subtext { .subtext {
color: var(--text-muted); color: var(--text-muted);
} }
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}