feat: Add avatar upload and fix authentication errors
Avatars: - Add avatar_path column to USERS table (migration 006) - Upload API endpoint /api/asteroid/user/avatar/upload - Profile page shows avatar with hover-to-change overlay - Default SVG avatar for users without uploaded image - Avatars stored in static/avatars/ directory Fixes: - 401 errors now return proper JSON instead of 500 - SQL escaping for history recording (single quotes) - Added debug logging for history/record API - Avatar container has background color for visibility For production: run migrations/006-user-avatars.sql
This commit is contained in:
parent
a2ebc415f2
commit
8f5fe7534d
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- Migration 006: User Avatars
|
||||||
|
-- Adds avatar support to user profiles
|
||||||
|
|
||||||
|
-- Add avatar_path column to USERS table
|
||||||
|
ALTER TABLE "USERS" ADD COLUMN IF NOT EXISTS avatar_path TEXT;
|
||||||
|
|
||||||
|
-- Grant permissions
|
||||||
|
GRANT ALL PRIVILEGES ON "USERS" TO asteroid;
|
||||||
|
|
||||||
|
-- Verification
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
RAISE NOTICE 'Migration 006: User avatars column added successfully!';
|
||||||
|
END $$;
|
||||||
|
|
@ -232,6 +232,45 @@
|
||||||
(defun load-more-favorites ()
|
(defun load-more-favorites ()
|
||||||
(show-message "Loading more favorites..." "info"))
|
(show-message "Loading more favorites..." "info"))
|
||||||
|
|
||||||
|
(defun load-avatar ()
|
||||||
|
(ps:chain
|
||||||
|
(fetch "/api/asteroid/user/avatar")
|
||||||
|
(then (lambda (response) (ps:chain response (json))))
|
||||||
|
(then (lambda (result)
|
||||||
|
(let ((data (or (ps:@ result data) result)))
|
||||||
|
(when (and (= (ps:@ data status) "success")
|
||||||
|
(ps:@ data avatar_path))
|
||||||
|
(let ((img (ps:chain document (get-element-by-id "user-avatar"))))
|
||||||
|
(when img
|
||||||
|
(setf (ps:@ img src) (ps:@ data avatar_path))))))))
|
||||||
|
(catch (lambda (error)
|
||||||
|
(ps:chain console (log "No avatar set or error loading:" error))))))
|
||||||
|
|
||||||
|
(defun upload-avatar (input)
|
||||||
|
(let ((file (ps:getprop (ps:@ input files) 0)))
|
||||||
|
(when file
|
||||||
|
(let ((form-data (ps:new (-form-data))))
|
||||||
|
(ps:chain form-data (append "avatar" file))
|
||||||
|
(ps:chain form-data (append "filename" (ps:@ file name)))
|
||||||
|
(show-message "Uploading avatar..." "info")
|
||||||
|
(ps:chain
|
||||||
|
(fetch "/api/asteroid/user/avatar/upload"
|
||||||
|
(ps:create :method "POST"
|
||||||
|
:body form-data))
|
||||||
|
(then (lambda (response) (ps:chain response (json))))
|
||||||
|
(then (lambda (result)
|
||||||
|
(let ((data (or (ps:@ result data) result)))
|
||||||
|
(if (= (ps:@ data status) "success")
|
||||||
|
(progn
|
||||||
|
(let ((img (ps:chain document (get-element-by-id "user-avatar"))))
|
||||||
|
(when img
|
||||||
|
(setf (ps:@ img src) (+ (ps:@ data avatar_path) "?" (ps:chain -date (now))))))
|
||||||
|
(show-message "Avatar updated!" "success"))
|
||||||
|
(show-message "Failed to upload avatar" "error")))))
|
||||||
|
(catch (lambda (error)
|
||||||
|
(ps:chain console (error "Error uploading avatar:" error))
|
||||||
|
(show-message "Error uploading avatar" "error"))))))))
|
||||||
|
|
||||||
(defun load-activity-chart ()
|
(defun load-activity-chart ()
|
||||||
(ps:chain
|
(ps:chain
|
||||||
(fetch "/api/asteroid/user/activity?days=30")
|
(fetch "/api/asteroid/user/activity?days=30")
|
||||||
|
|
@ -303,7 +342,8 @@
|
||||||
(load-recent-tracks)
|
(load-recent-tracks)
|
||||||
(load-favorites)
|
(load-favorites)
|
||||||
(load-top-artists)
|
(load-top-artists)
|
||||||
(load-activity-chart))
|
(load-activity-chart)
|
||||||
|
(load-avatar))
|
||||||
|
|
||||||
;; Action functions
|
;; Action functions
|
||||||
(defun load-more-recent-tracks ()
|
(defun load-more-recent-tracks ()
|
||||||
|
|
|
||||||
|
|
@ -1,83 +1,207 @@
|
||||||
#EXTM3U
|
#EXTM3U
|
||||||
#PLAYLIST:Midnight Ambient
|
#PLAYLIST:Morning Drift
|
||||||
#PHASE:Midnight Ambient
|
#PHASE:Morning Drift
|
||||||
#DURATION:6 hours (approx)
|
#DURATION:6 hours (approx)
|
||||||
#CURATOR:Asteroid Radio
|
#CURATOR:Asteroid Radio
|
||||||
#DESCRIPTION:Deep, dark ambient for the late night hours (00:00-06:00)
|
#DESCRIPTION:Lighter, awakening ambient for the morning hours (06:00-12:00)
|
||||||
|
|
||||||
#EXTINF:-1,Biosphere - The Petrified Forest
|
#EXTINF:-1,Brian Eno - Emerald And Lime
|
||||||
/app/music/Biosphere - The Petrified Forest (2017) - CD FLAC/04. Biosphere - The Petrified Forest.flac
|
/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/A1 Emerald And Lime.flac
|
||||||
#EXTINF:-1,Labradford - S
|
#EXTINF:-1,Tycho - Glider
|
||||||
/app/music/Labradford/1997 - Mi Media Naranja/1 S.flac
|
/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/01 - Glider.flac
|
||||||
#EXTINF:-1,Tim Hecker - Seasick
|
#EXTINF:-1,Biosphere - Drifter
|
||||||
/app/music/Tim Hecker - The North Water Original Score (2021 - WEB - FLAC)/Tim Hecker - The North Water (Original Score) - 01 Seasick.flac
|
/app/music/Biosphere - The Petrified Forest (2017) - CD FLAC/01. Biosphere - Drifter.flac
|
||||||
#EXTINF:-1,Pye Corner Audio - Hollow Earth
|
#EXTINF:-1,Four Tet - Alap
|
||||||
/app/music/Pye Corner Audio/2019 - Hollow Earth (WEB, #GBX032 DL)/01 - Hollow Earth.mp3
|
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/01 Alap.flac
|
||||||
#EXTINF:-1,Dead Voices On Air - Red Howls
|
#EXTINF:-1,Johann Johannsson - Cambridge, 1963
|
||||||
/app/music/Dead Voices On Air - Ghohst Stories (FLAC)/01 - Red Howls.flac
|
/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/01 - Cambridge, 1963.flac
|
||||||
#EXTINF:-1,Tangerine Dream - The Seventh Propellor of Silence
|
#EXTINF:-1,Ulrich Schnauss - Negative Sunrise (2019 Version)
|
||||||
/app/music/Tangerine Dream - Ambient Monkeys (flac)/03 Tangerine Dream - The Seventh Propellor of Silence.flac
|
/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/09. Negative Sunrise (2019 Version).flac
|
||||||
#EXTINF:-1,Biosphere - Fall Asleep For Me
|
#EXTINF:-1,Kiasmos - Lit
|
||||||
/app/music/Biosphere - Departed Glories (2016) - FLAC WEB/17 - Fall Asleep For Me.flac
|
/app/music/Kiasmos/2014 - Kiasmos/01 - Lit.flac
|
||||||
#EXTINF:-1,Locrian - Arc of Extinction
|
#EXTINF:-1,FSOL - Mountain Path
|
||||||
/app/music/Locrian - Infinite Dissolution (2015) - WEB FLAC/01 - Arc of Extinction.flac
|
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/02 - Mountain Path.flac
|
||||||
#EXTINF:-1,FSOL - Polarize
|
#EXTINF:-1,Brian Eno - Garden of Stars
|
||||||
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/01 - Polarize.flac
|
/app/music/Brian Eno/2022 - ForeverAndEverNoMore/04 Garden of Stars.flac
|
||||||
#EXTINF:-1,Marconi Union - Sleeper
|
#EXTINF:-1,Clark - Kiri's Glee
|
||||||
/app/music/Marconi Union - Ghost Stations (2016 - WEB - FLAC)/01. Marconi Union - Sleeper.flac
|
/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/04 - Kiri's Glee.flac
|
||||||
#EXTINF:-1,Labradford - G
|
#EXTINF:-1,Tycho - Source
|
||||||
/app/music/Labradford/1997 - Mi Media Naranja/2 G.flac
|
/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/07 - Source.flac
|
||||||
#EXTINF:-1,Biosphere - This Is The End
|
#EXTINF:-1,Biosphere - Out Of The Cradle
|
||||||
/app/music/Biosphere - The Petrified Forest (2017) - CD FLAC/06. Biosphere - This Is The End.flac
|
/app/music/Biosphere - Departed Glories (2016) - FLAC WEB/01 - Out Of The Cradle.flac
|
||||||
#EXTINF:-1,Pye Corner Audio - Descent
|
#EXTINF:-1,Tangerine Dream - Token from Birdland
|
||||||
/app/music/Pye Corner Audio/2019 - Hollow Earth (WEB, #GBX032 DL)/02 - Descent.mp3
|
/app/music/Tangerine Dream - Ambient Monkeys (flac)/01 Tangerine Dream - Token from Birdland.flac
|
||||||
#EXTINF:-1,Brian Eno - Reflection
|
#EXTINF:-1,Four Tet - Scientists
|
||||||
/app/music/Brian Eno/2017 - Reflection/01. Reflection.mp3
|
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/06 Scientists.flac
|
||||||
#EXTINF:-1,Dead Voices On Air - On Winters Gibbet
|
#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Solitary Falling
|
||||||
/app/music/Dead Voices On Air - Ghohst Stories (FLAC)/02 - On Winters Gibbet.flac
|
/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/03. Solitary Falling.flac
|
||||||
#EXTINF:-1,Tim Hecker - Left On The Ice
|
#EXTINF:-1,Proem - Modern Rope
|
||||||
/app/music/Tim Hecker - The North Water Original Score (2021 - WEB - FLAC)/Tim Hecker - The North Water (Original Score) - 05 Left On The Ice.flac
|
/app/music/Proem - 2018 Modern Rope (WEB)/05. Modern Rope.flac
|
||||||
#EXTINF:-1,Autechre - Further
|
#EXTINF:-1,Johann Johannsson - Rowing
|
||||||
/app/music/Autechre/1994 - Amber/08 Further.flac
|
/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/02 - Rowing.flac
|
||||||
#EXTINF:-1,Tangerine Dream - Moon Marble
|
#EXTINF:-1,Kiasmos - Held
|
||||||
/app/music/Tangerine Dream - Ambient Monkeys (flac)/06 Tangerine Dream - Moon Marble.flac
|
/app/music/Kiasmos/2014 - Kiasmos/02 - Held.flac
|
||||||
#EXTINF:-1,Biosphere - Sweet Dreams Form A Shade
|
#EXTINF:-1,Brian Eno - Complex Heaven
|
||||||
/app/music/Biosphere - Departed Glories (2016) - FLAC WEB/07 - Sweet Dreams Form A Shade.flac
|
/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/A2 Complex Heaven.flac
|
||||||
#EXTINF:-1,Locrian - Dark Shales
|
#EXTINF:-1,Biosphere - Skålbrekka
|
||||||
/app/music/Locrian - Infinite Dissolution (2015) - WEB FLAC/02 - Dark Shales.flac
|
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/01 - Skålbrekka.flac
|
||||||
#EXTINF:-1,FSOL - Forest Soundbed
|
#EXTINF:-1,FSOL - Thought Pattern
|
||||||
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/06 - Forest Soundbed.flac
|
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/03 - Thought Pattern.flac
|
||||||
#EXTINF:-1,Pye Corner Audio - Subterranean Lakes
|
#EXTINF:-1,Tycho - Into The Woods
|
||||||
/app/music/Pye Corner Audio/2019 - Hollow Earth (WEB, #GBX032 DL)/09 - Subterranean Lakes.mp3
|
/app/music/Tycho - Simulcast (2020) [WEB FLAC]/04 - Into The Woods.flac
|
||||||
#EXTINF:-1,Labradford - WR
|
#EXTINF:-1,arovane - hymn
|
||||||
/app/music/Labradford/1997 - Mi Media Naranja/3 WR.flac
|
/app/music/arovane - Wirkung (2020) [WEB FLAC16]/12. arovane - hymn.flac
|
||||||
#EXTINF:-1,Bark Psychosis - Hex
|
#EXTINF:-1,Four Tet - Green
|
||||||
/app/music/Bark Psychosis/1994 - Hex/01 The Loom.flac
|
/app/music/Four Tet - Sixteen Oceans (2020) {Text Records - TEXT051} [CD FLAC]/12 - Four Tet - Green.flac
|
||||||
#EXTINF:-1,Biosphere - Turned To Stone
|
#EXTINF:-1,Clark - Primary Pluck
|
||||||
/app/music/Biosphere - The Petrified Forest (2017) - CD FLAC/03. Biosphere - Turned To Stone.flac
|
/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/08 - Primary Pluck.flac
|
||||||
#EXTINF:-1,Tim Hecker - Winter's Coming
|
#EXTINF:-1,Biosphere - Strandby
|
||||||
/app/music/Tim Hecker - The North Water Original Score (2021 - WEB - FLAC)/Tim Hecker - The North Water (Original Score) - 10 Winter's Coming.flac
|
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/02 - Strandby.flac
|
||||||
#EXTINF:-1,Dead Voices On Air - On Wicca Way
|
#EXTINF:-1,Johann Johannsson - The Dreams That Stuff Is Made Of
|
||||||
/app/music/Dead Voices On Air - Ghohst Stories (FLAC)/03 - On Wicca Way.flac
|
/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/11 - The Dreams That Stuff Is Made Of.flac
|
||||||
#EXTINF:-1,Marconi Union - Abandoned - In Silence
|
#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Polychrome
|
||||||
/app/music/Marconi Union - Ghost Stations (2016 - WEB - FLAC)/03. Marconi Union - Abandoned - In Silence.flac
|
/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/08. Polychrome.flac
|
||||||
#EXTINF:-1,Pye Corner Audio - Deeper Dreaming
|
#EXTINF:-1,Kiasmos - Swayed
|
||||||
/app/music/Pye Corner Audio/2019 - Hollow Earth (WEB, #GBX032 DL)/13 - Deeper Dreaming.mp3
|
/app/music/Kiasmos/2014 - Kiasmos/04 - Swayed.flac
|
||||||
#EXTINF:-1,Tangerine Dream - Myopia World
|
#EXTINF:-1,Brian Eno - These Small Noises
|
||||||
/app/music/Tangerine Dream - Ambient Monkeys (flac)/12 Tangerine Dream - Myopia World.flac
|
/app/music/Brian Eno/2022 - ForeverAndEverNoMore/09 These Small Noises.flac
|
||||||
#EXTINF:-1,Biosphere - Departed Glories
|
#EXTINF:-1,Tycho - Ascension
|
||||||
/app/music/Biosphere - Departed Glories (2016) - FLAC WEB/09 - Departed Glories.flac
|
/app/music/Thievery Corporation and Tycho - Fragments Ascension EP (flac)/3. Tycho - Ascension.flac
|
||||||
#EXTINF:-1,Locrian - The Future of Death
|
#EXTINF:-1,FSOL - Imagined Friends
|
||||||
/app/music/Locrian - Infinite Dissolution (2015) - WEB FLAC/04 - The Future of Death.flac
|
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/15 - Imagined Friends.flac
|
||||||
#EXTINF:-1,Labradford - V
|
#EXTINF:-1,Biosphere - Lysbotn
|
||||||
/app/music/Labradford/1997 - Mi Media Naranja/6 V.flac
|
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/11 - Lysbotn.flac
|
||||||
#EXTINF:-1,FSOL - Solace
|
#EXTINF:-1,Boards of Canada - In a Beautiful Place Out in the Country
|
||||||
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/23 - Solace.flac
|
/app/music/Boards of Canada/In a Beautiful Place Out in the Country/01 Kid for Today.flac
|
||||||
#EXTINF:-1,Pye Corner Audio - Buried Memories
|
#EXTINF:-1,Brian Eno - Making Gardens Out of Silence
|
||||||
/app/music/Pye Corner Audio/2019 - Hollow Earth (WEB, #GBX032 DL)/12 - Buried Memories.mp3
|
/app/music/Brian Eno/2022 - ForeverAndEverNoMore/10 Making Gardens Out of Silence.flac
|
||||||
#EXTINF:-1,Tim Hecker - Twinkle In The Wasteland
|
#EXTINF:-1,Tangerine Dream - Virtue Is Its Own Reward
|
||||||
/app/music/Tim Hecker - The North Water Original Score (2021 - WEB - FLAC)/Tim Hecker - The North Water (Original Score) - 14 Twinkle In The Wasteland.flac
|
/app/music/Tangerine Dream - Ambient Monkeys (flac)/10 Tangerine Dream - Virtue Is Its Own Reward.flac
|
||||||
#EXTINF:-1,Locrian - Heavy Water
|
#EXTINF:-1,Brian Eno - Small Craft On A Milk Sea
|
||||||
/app/music/Locrian - Infinite Dissolution (2015) - WEB FLAC/08 - Heavy Water.flac
|
/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/A3 Small Craft On A Milk Sea.flac
|
||||||
#EXTINF:-1,Tape Loop Orchestra - 1953 Culture Festival
|
#EXTINF:-1,Tycho - Horizon
|
||||||
/app/music/Tape Loop Orchestra/2009 - 1953 Culture Festival/01 - 1953 Culture Festival.flac
|
/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/02 - Horizon.flac
|
||||||
|
#EXTINF:-1,Four Tet - Two Thousand And Seventeen
|
||||||
|
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/02 Two Thousand And Seventeen.flac
|
||||||
|
#EXTINF:-1,Boards of Canada - Dayvan Cowboy
|
||||||
|
/app/music/Boards of Canada/Trans Canada Highway/01 - Dayvan Cowboy.mp3
|
||||||
|
#EXTINF:-1,Ulrich Schnauss - Melts into Air (2019 Version)
|
||||||
|
/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/01. Melts into Air (2019 Version).flac
|
||||||
|
#EXTINF:-1,arovane - olopp_eleen
|
||||||
|
/app/music/arovane - Wirkung (2020) [WEB FLAC16]/01. arovane - olopp_eleen.flac
|
||||||
|
#EXTINF:-1,Biosphere - Bergsbotn
|
||||||
|
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/03 - Bergsbotn.flac
|
||||||
|
#EXTINF:-1,F.S.Blumm & Nils Frahm - Perff
|
||||||
|
/app/music/F.S Blumm and Nils Frahm - Music For Wobbling Music Versus Gravity (2013 - WEB - FLAC)/02. F.S.Blumm & Nils Frahm - Perff.flac
|
||||||
|
#EXTINF:-1,Clark - Simple Homecoming Loop
|
||||||
|
/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/02 - Simple Homecoming Loop.flac
|
||||||
|
#EXTINF:-1,Tycho - Slack
|
||||||
|
/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/03 - Slack.flac
|
||||||
|
#EXTINF:-1,Johann Johannsson - A Game Of Croquet
|
||||||
|
/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/07 - A Game Of Croquet.flac
|
||||||
|
#EXTINF:-1,FSOL - Motioned
|
||||||
|
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/04 - Motioned.flac
|
||||||
|
#EXTINF:-1,Brian Eno - Flint March
|
||||||
|
/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/A4 Flint March.flac
|
||||||
|
#EXTINF:-1,Four Tet - Lush
|
||||||
|
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/05 Lush.flac
|
||||||
|
#EXTINF:-1,Boards of Canada - Turquoise Hexagon Sun
|
||||||
|
/app/music/Boards of Canada/Hi Scores/02 - Turquoise Hexagon Sun.mp3
|
||||||
|
#EXTINF:-1,Ulrich Schnauss - Love Grows Out of Thin Air (2019 Version)
|
||||||
|
/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/02. Love Grows Out of Thin Air (2019 Version).flac
|
||||||
|
#EXTINF:-1,arovane - wirkung
|
||||||
|
/app/music/arovane - Wirkung (2020) [WEB FLAC16]/02. arovane - wirkung.flac
|
||||||
|
#EXTINF:-1,Biosphere - Berg
|
||||||
|
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/05 - Berg.flac
|
||||||
|
#EXTINF:-1,God is an Astronaut - Komorebi
|
||||||
|
/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/05. Komorebi.flac
|
||||||
|
#EXTINF:-1,Tycho - Weather
|
||||||
|
/app/music/Tycho - Simulcast (2020) [WEB FLAC]/01 - Weather.flac
|
||||||
|
#EXTINF:-1,Port Blue - Sunset Cruiser
|
||||||
|
/app/music/Port Blue - The Airship (2007)/04. Sunset Cruiser.flac
|
||||||
|
#EXTINF:-1,F.S.Blumm & Nils Frahm - Exercising Levitation
|
||||||
|
/app/music/F.S Blumm and Nils Frahm - Music For Wobbling Music Versus Gravity (2013 - WEB - FLAC)/07. F.S.Blumm & Nils Frahm - Exercising Levitation.flac
|
||||||
|
#EXTINF:-1,Four Tet - You Are Loved
|
||||||
|
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/08 You Are Loved.flac
|
||||||
|
#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Asteroid 2467
|
||||||
|
/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/01. Asteroid 2467.flac
|
||||||
|
#EXTINF:-1,Boards of Canada - Left Side Drive
|
||||||
|
/app/music/Boards of Canada/Trans Canada Highway/02 - Left Side Drive.mp3
|
||||||
|
#EXTINF:-1,Brian Eno - Who Gives a Thought
|
||||||
|
/app/music/Brian Eno/2022 - ForeverAndEverNoMore/01 Who Gives a Thought.flac
|
||||||
|
#EXTINF:-1,arovane - find
|
||||||
|
/app/music/arovane - Wirkung (2020) [WEB FLAC16]/03. arovane - find.flac
|
||||||
|
#EXTINF:-1,Tycho - Receiver
|
||||||
|
/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/04 - Receiver.flac
|
||||||
|
#EXTINF:-1,Johann Johannsson - The Origins Of Time
|
||||||
|
/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/08 - The Origins Of Time.flac
|
||||||
|
#EXTINF:-1,FSOL - Lichaen
|
||||||
|
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/05 - Lichaen.flac
|
||||||
|
#EXTINF:-1,Biosphere - Kyle
|
||||||
|
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/06 - Kyle.flac
|
||||||
|
#EXTINF:-1,Port Blue - The Grand Staircase
|
||||||
|
/app/music/Port Blue - The Airship (2007)/03. The Grand Staircase.flac
|
||||||
|
#EXTINF:-1,Vector Lovers - City Lights From A Train
|
||||||
|
/app/music/Vector Lovers/2005 - Capsule For One/01 - City Lights From A Train.mp3
|
||||||
|
#EXTINF:-1,Four Tet - Teenage Birdsong
|
||||||
|
/app/music/Four Tet - Sixteen Oceans (2020) {Text Records - TEXT051} [CD FLAC]/04 - Four Tet - Teenage Birdsong.flac
|
||||||
|
#EXTINF:-1,Ulrich Schnauss - The Magic in You (2019 Version)
|
||||||
|
/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/03. The Magic in You (2019 Version).flac
|
||||||
|
#EXTINF:-1,Boards of Canada - Oirectine
|
||||||
|
/app/music/Boards of Canada/Twoism/02 - Oirectine.mp3
|
||||||
|
#EXTINF:-1,Clark - Bench
|
||||||
|
/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/03 - Bench.flac
|
||||||
|
#EXTINF:-1,Tycho - Alright
|
||||||
|
/app/music/Tycho - Simulcast (2020) [WEB FLAC]/02 - Alright.flac
|
||||||
|
#EXTINF:-1,Brian Eno - Lesser Heaven
|
||||||
|
/app/music/Brian Eno/2011 - Small Craft On a Milk Sea/C3 Lesser Heaven.flac
|
||||||
|
#EXTINF:-1,arovane - sloon
|
||||||
|
/app/music/arovane - Wirkung (2020) [WEB FLAC16]/05. arovane - sloon.flac
|
||||||
|
#EXTINF:-1,God is an Astronaut - Epitaph
|
||||||
|
/app/music/God is an Astronaut - Epitaph (2018) WEB FLAC/01. Epitaph.flac
|
||||||
|
#EXTINF:-1,F.S.Blumm & Nils Frahm - Silently Sharing
|
||||||
|
/app/music/F.S Blumm and Nils Frahm - Music For Wobbling Music Versus Gravity (2013 - WEB - FLAC)/10. F.S.Blumm & Nils Frahm - Silently Sharing.flac
|
||||||
|
#EXTINF:-1,Biosphere - Fjølhøgget
|
||||||
|
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/07 - Fjølhøgget.flac
|
||||||
|
#EXTINF:-1,Port Blue - Over Atlantic City
|
||||||
|
/app/music/Port Blue - The Airship (2007)/02. Over Atlantic City.flac
|
||||||
|
#EXTINF:-1,Johann Johannsson - Viva Voce
|
||||||
|
/app/music/Johann Johannsson - The Theory of Everything (2014) [FLAC]/09 - Viva Voce.flac
|
||||||
|
#EXTINF:-1,Tangerine Dream - Symphony in A-minor
|
||||||
|
/app/music/Tangerine Dream - Ambient Monkeys (flac)/02 Tangerine Dream - Symphony in A-minor.flac
|
||||||
|
#EXTINF:-1,Tycho - Epoch
|
||||||
|
/app/music/Tycho - Epoch (Deluxe Version) (2019) [WEB FLAC16-44.1]/05 - Epoch.flac
|
||||||
|
#EXTINF:-1,Four Tet - Memories
|
||||||
|
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/11 Memories.flac
|
||||||
|
#EXTINF:-1,Ulrich Schnauss & Jonas Munk - Return To Burlington
|
||||||
|
/app/music/Ulrich Schnauss & Jonas Munk - Eight Fragments Of An Illusion (2021) - WEB FLAC/02. Return To Burlington.flac
|
||||||
|
#EXTINF:-1,Boards of Canada - Skyliner
|
||||||
|
/app/music/Boards of Canada/Trans Canada Highway/04 - Skyliner.mp3
|
||||||
|
#EXTINF:-1,Vector Lovers - Melodies And Memory
|
||||||
|
/app/music/Vector Lovers/2005 - Capsule For One/07 - Melodies And Memory.mp3
|
||||||
|
#EXTINF:-1,Brian Eno - Icarus or Blériot
|
||||||
|
/app/music/Brian Eno/2022 - ForeverAndEverNoMore/03 Icarus or Blériot.flac
|
||||||
|
#EXTINF:-1,arovane - noondt
|
||||||
|
/app/music/arovane - Wirkung (2020) [WEB FLAC16]/07. arovane - noondt.flac
|
||||||
|
#EXTINF:-1,FSOL - Symphony for Halia
|
||||||
|
/app/music/The Future Sound of London - Environment Six (2016 - WEB - FLAC)/14 - Symphony for Halia.flac
|
||||||
|
#EXTINF:-1,Biosphere - Straumen
|
||||||
|
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/12 - Straumen.flac
|
||||||
|
#EXTINF:-1,Port Blue - The Gentle Descent
|
||||||
|
/app/music/Port Blue - The Airship (2007)/12. The Gentle Descent.flac
|
||||||
|
#EXTINF:-1,Proem - As They Go
|
||||||
|
/app/music/Proem/2019 - As They Go/Proem - As They Go - 01 As They Go.flac
|
||||||
|
#EXTINF:-1,Tycho - Outer Sunset
|
||||||
|
/app/music/Tycho - Simulcast (2020) [WEB FLAC]/03 - Outer Sunset.flac
|
||||||
|
#EXTINF:-1,Four Tet - Planet
|
||||||
|
/app/music/Four Tet - New Energy {CD} [FLAC] (2017)/14 Planet.flac
|
||||||
|
#EXTINF:-1,Ulrich Schnauss - New Day Starts at Dawn (2019 Version)
|
||||||
|
/app/music/Ulrich Schnauss - No Further Ahead Than Tomorrow (2020) - WEB FLAC/07. New Day Starts at Dawn (2019 Version).flac
|
||||||
|
#EXTINF:-1,Clark - Goodnight Kiri
|
||||||
|
/app/music/Clark - Kiri Variations (2019) [WEB FLAC]/14 - Goodnight Kiri.flac
|
||||||
|
#EXTINF:-1,Biosphere - Steinfjord
|
||||||
|
/app/music/Biosphere - The Senja Recordings (2019) [FLAC]/13 - Steinfjord.flac
|
||||||
|
#EXTINF:-1,Tangerine Dream - Calyx Calamander
|
||||||
|
/app/music/Tangerine Dream - Ambient Monkeys (flac)/04 Tangerine Dream - Calyx Calamander.flac
|
||||||
|
#EXTINF:-1,Vector Lovers - To The Stars
|
||||||
|
/app/music/Vector Lovers/2005 - Capsule For One/12 - To The Stars.mp3
|
||||||
|
|
|
||||||
|
|
@ -1703,6 +1703,57 @@ body.popout-body .status-mini{
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-header{
|
||||||
|
display: flex;
|
||||||
|
gap: 30px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-section{
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-container{
|
||||||
|
position: relative;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 3px solid #00cc00;
|
||||||
|
cursor: pointer;
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-image{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-overlay{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: #00cc00;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
opacity: 0;
|
||||||
|
-moz-transition: opacity 0.2s ease;
|
||||||
|
-o-transition: opacity 0.2s ease;
|
||||||
|
-webkit-transition: opacity 0.2s ease;
|
||||||
|
-ms-transition: opacity 0.2s ease;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.avatar-container:hover .avatar-overlay{
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.activity-chart{
|
.activity-chart{
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1360,6 +1360,47 @@
|
||||||
:padding "4px 8px"
|
:padding "4px 8px"
|
||||||
:font-size "0.8em"))
|
:font-size "0.8em"))
|
||||||
|
|
||||||
|
;; Avatar styling
|
||||||
|
(.profile-header
|
||||||
|
:display "flex"
|
||||||
|
:gap "30px"
|
||||||
|
:align-items "flex-start")
|
||||||
|
|
||||||
|
(.avatar-section
|
||||||
|
:flex-shrink "0")
|
||||||
|
|
||||||
|
(.avatar-container
|
||||||
|
:position "relative"
|
||||||
|
:width "120px"
|
||||||
|
:height "120px"
|
||||||
|
:border-radius "50%"
|
||||||
|
:overflow "hidden"
|
||||||
|
:border "3px solid #00cc00"
|
||||||
|
:cursor "pointer"
|
||||||
|
:background "#1a1a1a")
|
||||||
|
|
||||||
|
(.avatar-image
|
||||||
|
:width "100%"
|
||||||
|
:height "100%"
|
||||||
|
:object-fit "cover")
|
||||||
|
|
||||||
|
(.avatar-overlay
|
||||||
|
:position "absolute"
|
||||||
|
:bottom "0"
|
||||||
|
:left "0"
|
||||||
|
:right "0"
|
||||||
|
:background "rgba(0, 0, 0, 0.7)"
|
||||||
|
:color "#00cc00"
|
||||||
|
:text-align "center"
|
||||||
|
:padding "8px"
|
||||||
|
:font-size "0.8em"
|
||||||
|
:opacity "0"
|
||||||
|
:transition "opacity 0.2s ease")
|
||||||
|
|
||||||
|
((:and .avatar-container :hover)
|
||||||
|
(.avatar-overlay
|
||||||
|
:opacity "1"))
|
||||||
|
|
||||||
;; Activity chart styling
|
;; Activity chart styling
|
||||||
(.activity-chart
|
(.activity-chart
|
||||||
:padding "15px"
|
:padding "15px"
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
||||||
|
<circle cx="60" cy="60" r="58" fill="#1a1a1a" stroke="#00cc00" stroke-width="2"/>
|
||||||
|
<circle cx="60" cy="45" r="20" fill="#00cc00"/>
|
||||||
|
<path d="M 25 95 Q 25 70 60 70 Q 95 70 95 95" fill="#00cc00"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 294 B |
|
|
@ -21,22 +21,33 @@
|
||||||
<!-- User Profile Header -->
|
<!-- User Profile Header -->
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
<h2>🎧 User Profile</h2>
|
<h2>🎧 User Profile</h2>
|
||||||
<div class="profile-info">
|
<div class="profile-header">
|
||||||
<div class="info-group">
|
<div class="avatar-section">
|
||||||
<span class="info-label">Username:</span>
|
<div class="avatar-container" id="avatar-container">
|
||||||
<span class="info-value" data-text="username">user</span>
|
<img id="user-avatar" src="/asteroid/static/icons/default-avatar.svg" alt="User Avatar" class="avatar-image">
|
||||||
|
<div class="avatar-overlay" onclick="document.getElementById('avatar-input').click()">
|
||||||
|
<span>Change</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="avatar-input" accept="image/*" style="display: none" onchange="uploadAvatar(this)">
|
||||||
</div>
|
</div>
|
||||||
<div class="info-group">
|
<div class="profile-info">
|
||||||
<span class="info-label">Role:</span>
|
<div class="info-group">
|
||||||
<span class="info-value" data-text="user-role">listener</span>
|
<span class="info-label">Username:</span>
|
||||||
</div>
|
<span class="info-value" data-text="username">user</span>
|
||||||
<div class="info-group">
|
</div>
|
||||||
<span class="info-label">Member Since:</span>
|
<div class="info-group">
|
||||||
<span class="info-value" data-text="join-date">2024-01-01</span>
|
<span class="info-label">Role:</span>
|
||||||
</div>
|
<span class="info-value" data-text="user-role">listener</span>
|
||||||
<div class="info-group">
|
</div>
|
||||||
<span class="info-label">Last Active:</span>
|
<div class="info-group">
|
||||||
<span class="info-value" data-text="last-active">Today</span>
|
<span class="info-label">Member Since:</span>
|
||||||
|
<span class="info-value" data-text="join-date">2024-01-01</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<span class="info-label">Last Active:</span>
|
||||||
|
<span class="info-value" data-text="last-active">Today</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -174,12 +174,12 @@
|
||||||
;; Not authenticated - emit error and signal to stop processing
|
;; Not authenticated - emit error and signal to stop processing
|
||||||
(progn
|
(progn
|
||||||
(if is-api-request
|
(if is-api-request
|
||||||
;; API request - emit JSON error with 401 status
|
;; API request - return JSON error with 401 status using api-output
|
||||||
(progn
|
(progn
|
||||||
(format t "Authentication failed - returning JSON 401~%")
|
(format t "Authentication failed - returning JSON 401~%")
|
||||||
(setf (radiance:return-code *response*) 401)
|
(api-output `(("status" . "error")
|
||||||
(setf (radiance:content-type *response*) "application/json")
|
("message" . "Authentication required"))
|
||||||
(error 'radiance:request-denied :message "Authentication required"))
|
:status 401))
|
||||||
;; Page request - redirect to login
|
;; Page request - redirect to login
|
||||||
(progn
|
(progn
|
||||||
(format t "Authentication failed - redirecting to login~%")
|
(format t "Authentication failed - redirecting to login~%")
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,12 @@
|
||||||
;;; Listening History - Per-user track play history
|
;;; Listening History - Per-user track play history
|
||||||
;;; ==========================================================================
|
;;; ==========================================================================
|
||||||
|
|
||||||
|
(defun sql-escape-string (str)
|
||||||
|
"Escape a string for SQL by doubling single quotes"
|
||||||
|
(if str
|
||||||
|
(cl-ppcre:regex-replace-all "'" str "''")
|
||||||
|
""))
|
||||||
|
|
||||||
(defun record-listen (user-id &key track-id track-title (duration 0) (completed nil))
|
(defun record-listen (user-id &key track-id track-title (duration 0) (completed nil))
|
||||||
"Record a track listen in user's history. Can use track-id or track-title."
|
"Record a track listen in user's history. Can use track-id or track-title."
|
||||||
(with-db
|
(with-db
|
||||||
|
|
@ -80,12 +86,12 @@
|
||||||
(postmodern:query
|
(postmodern:query
|
||||||
(:raw (format nil "INSERT INTO listening_history (\"user-id\", \"track-id\", track_title, \"listen-duration\", completed) VALUES (~a, ~a, ~a, ~a, ~a)"
|
(:raw (format nil "INSERT INTO listening_history (\"user-id\", \"track-id\", track_title, \"listen-duration\", completed) VALUES (~a, ~a, ~a, ~a, ~a)"
|
||||||
user-id track-id
|
user-id track-id
|
||||||
(if track-title (format nil "$$~a$$" track-title) "NULL")
|
(if track-title (format nil "'~a'" (sql-escape-string track-title)) "NULL")
|
||||||
duration (if completed 1 0))))
|
duration (if completed 1 0))))
|
||||||
(when track-title
|
(when track-title
|
||||||
(postmodern:query
|
(postmodern:query
|
||||||
(:raw (format nil "INSERT INTO listening_history (\"user-id\", track_title, \"listen-duration\", completed) VALUES (~a, $$~a$$, ~a, ~a)"
|
(:raw (format nil "INSERT INTO listening_history (\"user-id\", track_title, \"listen-duration\", completed) VALUES (~a, '~a', ~a, ~a)"
|
||||||
user-id track-title duration (if completed 1 0))))))))
|
user-id (sql-escape-string track-title) duration (if completed 1 0))))))))
|
||||||
|
|
||||||
(defun get-listening-history (user-id &key (limit 20) (offset 0))
|
(defun get-listening-history (user-id &key (limit 20) (offset 0))
|
||||||
"Get user's listening history - works with title-based history"
|
"Get user's listening history - works with title-based history"
|
||||||
|
|
@ -230,8 +236,10 @@
|
||||||
(parse-integer track-id :junk-allowed t)))
|
(parse-integer track-id :junk-allowed t)))
|
||||||
(duration-int (if duration (parse-integer duration :junk-allowed t) 0))
|
(duration-int (if duration (parse-integer duration :junk-allowed t) 0))
|
||||||
(completed-bool (and completed (string-equal completed "true"))))
|
(completed-bool (and completed (string-equal completed "true"))))
|
||||||
(record-listen user-id :track-id track-id-int :track-title title
|
(format t "Recording listen: user-id=~a title=~a~%" user-id title)
|
||||||
:duration (or duration-int 0) :completed completed-bool)
|
(when (and user-id title)
|
||||||
|
(record-listen user-id :track-id track-id-int :track-title title
|
||||||
|
:duration (or duration-int 0) :completed completed-bool))
|
||||||
(api-output `(("status" . "success")
|
(api-output `(("status" . "success")
|
||||||
("message" . "Listen recorded"))))))
|
("message" . "Listen recorded"))))))
|
||||||
|
|
||||||
|
|
@ -256,3 +264,64 @@
|
||||||
`(("day" . ,(cdr (assoc :day a)))
|
`(("day" . ,(cdr (assoc :day a)))
|
||||||
("track_count" . ,(cdr (assoc :track-count a)))))
|
("track_count" . ,(cdr (assoc :track-count a)))))
|
||||||
activity)))))))
|
activity)))))))
|
||||||
|
|
||||||
|
;;; ==========================================================================
|
||||||
|
;;; Avatar Management
|
||||||
|
;;; ==========================================================================
|
||||||
|
|
||||||
|
(defun get-avatars-directory ()
|
||||||
|
"Get the path to the avatars directory"
|
||||||
|
(merge-pathnames "static/avatars/" (asdf:system-source-directory :asteroid)))
|
||||||
|
|
||||||
|
(defun save-avatar (user-id temp-file-path original-filename)
|
||||||
|
"Save an avatar file from temp path and return the relative path"
|
||||||
|
(let* ((extension (pathname-type original-filename))
|
||||||
|
(safe-ext (if (member extension '("png" "jpg" "jpeg" "gif" "webp") :test #'string-equal)
|
||||||
|
extension
|
||||||
|
"png"))
|
||||||
|
(new-filename (format nil "~a.~a" user-id safe-ext))
|
||||||
|
(full-path (merge-pathnames new-filename (get-avatars-directory)))
|
||||||
|
(relative-path (format nil "/asteroid/static/avatars/~a" new-filename)))
|
||||||
|
;; Copy from temp file to avatars directory
|
||||||
|
(uiop:copy-file temp-file-path full-path)
|
||||||
|
;; Update database
|
||||||
|
(with-db
|
||||||
|
(postmodern:query
|
||||||
|
(:raw (format nil "UPDATE \"USERS\" SET avatar_path = '~a' WHERE _id = ~a"
|
||||||
|
relative-path user-id))))
|
||||||
|
relative-path))
|
||||||
|
|
||||||
|
(defun get-user-avatar (user-id)
|
||||||
|
"Get the avatar path for a user"
|
||||||
|
(with-db
|
||||||
|
(postmodern:query
|
||||||
|
(:raw (format nil "SELECT avatar_path FROM \"USERS\" WHERE _id = ~a" user-id))
|
||||||
|
:single)))
|
||||||
|
|
||||||
|
(define-api asteroid/user/avatar/upload () ()
|
||||||
|
"Upload a new avatar image"
|
||||||
|
(require-authentication)
|
||||||
|
(with-error-handling
|
||||||
|
(let* ((user-id (session:field "user-id"))
|
||||||
|
;; Radiance wraps hunchentoot - post-var returns (path filename content-type) for files
|
||||||
|
(file-info (radiance:post-var "avatar"))
|
||||||
|
(temp-path (when (listp file-info) (first file-info)))
|
||||||
|
(original-name (when (listp file-info) (second file-info))))
|
||||||
|
(format t "Avatar upload: file-info=~a temp-path=~a original-name=~a~%" file-info temp-path original-name)
|
||||||
|
(if (and temp-path (probe-file temp-path))
|
||||||
|
(let ((avatar-path (save-avatar user-id temp-path (or original-name "avatar.png"))))
|
||||||
|
(api-output `(("status" . "success")
|
||||||
|
("message" . "Avatar uploaded successfully")
|
||||||
|
("avatar_path" . ,avatar-path))))
|
||||||
|
(api-output `(("status" . "error")
|
||||||
|
("message" . "No file provided"))
|
||||||
|
:status 400)))))
|
||||||
|
|
||||||
|
(define-api asteroid/user/avatar () ()
|
||||||
|
"Get current user's avatar path"
|
||||||
|
(require-authentication)
|
||||||
|
(with-error-handling
|
||||||
|
(let* ((user-id (session:field "user-id"))
|
||||||
|
(avatar-path (get-user-avatar user-id)))
|
||||||
|
(api-output `(("status" . "success")
|
||||||
|
("avatar_path" . ,avatar-path))))))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue