From 5beb927bf03fa9e2a4d26a5097a40d284baeaa56 Mon Sep 17 00:00:00 2001 From: Neythen Date: Mon, 11 Oct 2021 20:16:04 +0100 Subject: [PATCH] images, gifs and messages added to frontend --- csv/GIF_settings.json | 2 +- csv/display_settings.json | 2 +- csv/message_settings.json | 1 + display_images/Current Weather.ppm | Bin 95054 -> 95054 bytes display_images/Daily Forecast.ppm | Bin 136527 -> 136527 bytes log.txt | 17 --- server.py | 8 +- static/app.js | 86 +++++++++--- stockTicker.py | 206 ++++++++++++++++++----------- templates/index.html | 62 +++++---- user_uploads/close.gif | Bin 0 -> 821 bytes 11 files changed, 237 insertions(+), 147 deletions(-) create mode 100644 csv/message_settings.json create mode 100644 user_uploads/close.gif diff --git a/csv/GIF_settings.json b/csv/GIF_settings.json index 1386e94..09ecce7 100644 --- a/csv/GIF_settings.json +++ b/csv/GIF_settings.json @@ -1 +1 @@ -{"speed": "medium", "animation": "continuous", "title": true, "pause": "", "images": ["open.gif"]} \ No newline at end of file +{"speed": "medium", "animation": "continuous", "title": true, "pause": "", "images": ["open.gif", "close.gif"]} \ No newline at end of file diff --git a/csv/display_settings.json b/csv/display_settings.json index 1ed1427..43da6fc 100755 --- a/csv/display_settings.json +++ b/csv/display_settings.json @@ -1 +1 @@ -["Custom Images"] \ No newline at end of file +["Custom Messages"] \ No newline at end of file diff --git a/csv/message_settings.json b/csv/message_settings.json new file mode 100644 index 0000000..03906d3 --- /dev/null +++ b/csv/message_settings.json @@ -0,0 +1 @@ +{"feature": "Custom Messages", "speed": "Medium", "animation": "Continuous", "title": true, "messages": [{"name": "1", "text": "123", "text_colour": "White", "size": "Medium", "background_colour": "Black"}, {"name": "13", "text": "123", "text_colour": "Red", "size": "Medium", "background_colour": "Purple"}]} \ No newline at end of file diff --git a/display_images/Current Weather.ppm b/display_images/Current Weather.ppm index 4dacfbc6f9592df307821a134ecfd0cf83693515..eaa060eded6a237c047c31147706efc1d0257b1c 100755 GIT binary patch delta 1311 zcmah}(MwZN9Of){^ypc4yKXVNG|$jlNaiS(i7tkwA%(0VL}j&yD5a1+NRS~3VGq@V z^Lw~G^spcZigM#TSb@GL`^AQ8_9s{X^9I3fbT8tv^XP9IZGX}#|7sa#@M5~+1`*l;Kdd-DO&A9;s zYcYn5i^C#(;iL}hP5{Z%+`LjxbfnAx<~^EeG2$~bBSazVSF4kbBGawmr=Q6No`GVk7|38LBnck~I{)gAmVng{^WZ(Wc({6wP_l>L-e;BT&;~PFy6i z*f#7ab+noTu-w#)?7hs+BhXYb9^B ziq0OtIoqq!YDBy0G{T_@c;k- delta 1098 zcmZ{jPiPZC6vmm@Ox-wT(`_ONi|fP|D@AQkYK1nk#->nuFa}Tl&8b)lB}f&qs1eaq zvE6)!_240bASl>`ITVqKC-qdKw|Y=dy|#y5ycplcq>!2%mSLIqz4!axd$YFTU)%7P z=Jp_4=ttV(i2F}EatgC%1lPJa?wTlgwZQvK3#V&yg6NS?T zDz`0U$Ao*elNrdl7K%~bt!ONa%6@@O=!J!(BU4!Fwjgy*l{MG_(Oh8)#bF&Yrl!)` zi>B&jnP=*&Ncd2i>~og_EQYL4bL1UTXmu71vi}tEaQqI1vD~#vg>?E%M;=3^XLOhJ zeoY!5ilJn3e4sRmsHIBA6}BtTs}X|?3sns*G+&nGB2zaawhzH=6I*@*GNmIvrXlzF ztRrnTYrbQx@5bxoBnfk5kCLQg2Lm7TK{qbhUgVQutnSrR-a6bSk=?*UHI#yUF5J=q zrZt4fTen~zG04*ew~}Pk{ec*A@57E1ULQ>qJGn<&mKT|-_D=Y4{t!(zN{&;yo3)t^ zdt$y0>y2Dfvi}up74>kJB~ky^X&3}xj4C-mF52XOLuB6?h!!<7f?S}+SetcOQtzw1 zJ;-MS%)Wit6dZ$n8j2Z?@6(ezgz0z;KZgz2Q4L!mj>|-RFMgdtas$DqMq!Z_rHwV( zpfGKd2h3?LgxWeW(@t1(tKvhi+Xc+4;xJ9WMOkG}EP7@FDlwAuA)Pdl3ko+s#^^mg zj!?sIHr}JUbmk>%72$2Az>g83*6r32UJlKB(`w?J`acIjJgGq-F(Rm|-D-RMxKEWP hSGXfT(fQ@yZSWb#+i8L2K@FSZ{zpz`#mTT$`4^+Tb*KOU diff --git a/display_images/Daily Forecast.ppm b/display_images/Daily Forecast.ppm index 9f044189d1f1182778e76a846f5f708c97ecb6f9..77ae5be62500a0136930954445b93e84efcffd78 100755 GIT binary patch delta 1473 zcma)6Uuaup6z8VxZNK%#FT3`(Swq9M>l$Y_cVX3~b~RVGNY-rj3dOnjrx(WNkZB`j zwlFs%I`K(v@)Iu`(*}G{ihrWtLqQSilOkve6Cb6IR)*%mfg-k^FNq~7h4ta`!9C}9 z&hMP_JEy$RQC{dMy&Zt_EBCzv3ip=^${`dlGdQzW-t5GQyLEK7Ul-+;_Z0jvsDsBC z4yQB}+m@P8M>e5EXM2AD!QC1@p3pIW4};4-Y(i|2XP)wWpui*VPjnRzb{psmE^Y_4 zpnPTyD)@cp-*9GBgvL3FqdMG}24@#X)(rbAuPR~Wu5c{(Cg6Oj;pLkeriN9N2RMOR zVMoqY!Pfoc6W6hF{LZ}Dfi+u4XfH$dJ1&Y{vyz^Qse2jL6pbiexLraWzpSg+H^hbA zb1gvVFqZowIMB_p(xVpAI_%!a?SgQz-21HrvqB=h;k+Fnm7A3P5S8fF7I>4bxZ3{@ zimP{3doVT3;l)zO4UHfqI9%$5H=@(rFJPjJ7|!UZWH_F-qHw4>JPHpxj1}Vb)pZzRK%f|bbB^mEPNF%^!@^X3jwieIjc#$`q= z3fl~&jQf;oEGw*`^t}e}F@w|*gZU(bmDa=yW0$JpEhH{B<`naZ(bjq$f5~~1GCxlm zEe-oLSN~l|y3`1bqN%WZcHwJhugD#HqTWOXdz9n(qgo-su&yzwALJgOo}~sh0?bRW zU-?NH5Mw6@SzE!2b-S>gqms*deFJJu$ezm73N!ZLmE@q9c>G2{0_;;H%<868{RH{7 zZ1Eq>_&!8QF{NQz`SY}fw=q^6wWh`Y!!E#_>&t=bq!q(4%2hocR~t>b&}B( z9>x;M)OvkCg5&gBOjxJaXd;u{aL(MKGh)C!Ezi!QD8<`QdhRRVoK{qPx=I(~p=jO= z!&SNXsd$9F(UeDsoqz3N(0; z)e!sNyiv0v8`of7Q+;PzF<{d!~E_xt8Am(E`<&3t0hHQQYq7INj7Z*V|tXH>%l5FHm9H5;Ee*<$4{;_?qn6kyVOw?JSzuSORtk6y6aMRze33&$en{UX zP%4#`LRXYlhV$E(32iO3Pk98Z1mt>FWaJU^c9~I8e+BZA8?)V=de&pN`kh!mGWUjz z8Sj4N8zp29Nk$~Ho;D~iNqVj+W05TCd3@bOddz#<65mqf^t!tVaIdblUU-gtWXHZ; zkEr`197lP>1^T^?os0w&lFgi&*-zb%MxCd4WO1NJS>}Zb_Ui!OTqWIsE30oOBV^@VFBsw70C9A znAjy^N^L>BnZr#c;oSpc1xccP4#YYt$P--gk<{D=tPYvoJX*VX`q_zSwP3pPV%t`v zyWd9KEkPsaeM@tB_I(f8o5n$Qnv_kOn|61S3weyS(ZO(#RBIQDlKf%PdVG|OZ;V85 z?f!1HMMgIcF~IA&o*xTgVtpB%Nbmi>FyidT0<|(eN}NK5vyZEqm+^+B_^;xx7c`!} zCg6+S2gT_rB~@rVM@(fBiq9acM*n1VYB?QP?optP)FIqdrpNlyHfW17GRJ5hf0s$V zS~Y)%h3Ke|X{yD!{@!Cj+}T2xfvBeMFmmYu84krm zz<6ZZC4FFI%5F~U^fi$bGcs1_Q4AiAwWeyB_ z%XP=pEu*l6g$W;3(`RQ+F{5hUC+H1{e^#x@#Wk8=T1!ozDP5^SoxIOM`MK7trTR8A sa{F6Ew@0SSi5Qxz9@3=NxUs0$yG1oSY<`qrW~%O7zk0P_WlLxO13=W@IRF3v diff --git a/log.txt b/log.txt index f74d4c2..e69de29 100755 --- a/log.txt +++ b/log.txt @@ -1,17 +0,0 @@ -[Errno 2] No such file or directory: '/home/pi/Desktop/stock_ticker/XOM.png'. file: stockTicker.py. line: 2230. type: - Traceback (most recent call last): - File "stockTicker.py", line 2230, in - stock_ticker.process_msg(msg) - File "stockTicker.py", line 2194, in process_msg - self.scrollFunctionsAnimated(userSettings, animation = 'down') - File "stockTicker.py", line 309, in scrollFunctionsAnimated - self.updateMultiple([options[0]]) - File "stockTicker.py", line 276, in updateMultiple - img = self.functions[option]() - File "stockTicker.py", line 1953, in getUserImage - image = self.openImage(os.path.join(os.path.dirname(os.path.abspath(__file__)), image)) - File "stockTicker.py", line 76, in openImage - image = Image.open(image_file) - File "/usr/lib/python3/dist-packages/PIL/Image.py", line 2634, in open - fp = builtins.open(filename, "rb") -FileNotFoundError: [Errno 2] No such file or directory: '/home/pi/Desktop/stock_ticker/XOM.png' diff --git a/server.py b/server.py index 5fd3e68..e639fe4 100644 --- a/server.py +++ b/server.py @@ -365,6 +365,9 @@ def feature_settings(): elif feature in ['Custom GIFs', 'Custom Images']: save_image_settings(input_settings) + elif feature == 'Custom Messages': + save_message_settings(input_settings) + return index() # saves files uploaded to the webpage for images and GIFs @@ -503,7 +506,10 @@ def save_image_settings(input_settings): del current_settings['feature'] json.dump(current_settings, open('csv/' + filename, 'w+')) - +def save_message_settings(input_settings): + + json.dump(input_settings, open('csv/message_settings.json', 'w+')) + @app.route("/stop") def stop(): print('stop') diff --git a/static/app.js b/static/app.js index ac3347a..a40cd4e 100644 --- a/static/app.js +++ b/static/app.js @@ -489,26 +489,7 @@ var allFeaturesFileAddBtn = [ -var uploaded_images = []; -var uploaded_GIFs = []; -allFeaturesFileAddBtn.map((value, index) => { - if (value !== null) { - value.addEventListener("click", () => { - var tag = document.createElement("li"); - tag.innerHTML = allFeaturesFile[index].files[0].name; - if (index == 10) { - uploaded_images.push(allFeaturesFile[index].files[0]); - } else if (index == 11) { - uploaded_GIFs.push(allFeaturesFile[index].files[0]); - } - - allFeatures[index].appendChild(tag); - changeVarValue(); - addEventOnFeaturesList(); - }); - } -}); // features input text var stocksText = document.getElementById("inputText3"); @@ -724,7 +705,7 @@ function saveSettings() { method:"POST", body:data }); - } + } } let saveSettingsButtons = document.querySelectorAll(".save-btn-div").forEach(button => @@ -801,6 +782,29 @@ function getSportsSettings(page){ return settings; } + +var uploaded_images = []; +var uploaded_GIFs = []; + +allFeaturesFileAddBtn.map((value, index) => { + if (value !== null) { + value.addEventListener("click", () => { + var tag = document.createElement("li"); + tag.innerHTML = allFeaturesFile[index].files[0].name; + if (index == 10) { + uploaded_images.push(allFeaturesFile[index].files[0]); + } else if (index == 11) { + uploaded_GIFs.push(allFeaturesFile[index].files[0]); + } + + allFeatures[index].appendChild(tag); + changeVarValue(); + addEventOnFeaturesList(); + }); + } +}); + + //images and GIFs function getImageSettings(page){ let pause = page.querySelectorAll(".pause-select")[0].value; @@ -814,8 +818,48 @@ function getImageSettings(page){ return settings; } -function getMessageSettings(page) { + +var messages = []; + + +messagesTextAddBtn.addEventListener("click", () => { + let pageSelector = "Page13"; + let page = document.getElementById(pageSelector); + + + let msg_name = messagesText.value; + //let speed = getSelected(page.querySelectorAll(".speed-select")[0]); + //let animation = getSelected(page.querySelectorAll(".animation-select")[0]); + + let message_text = page.querySelectorAll(".message-input")[0].value; + let text_colour = getSelected(page.querySelectorAll(".text-colour")[0]); + let text_size = getSelected(page.querySelectorAll(".text-size")[0]); + let background_colour = getSelected(page.querySelectorAll(".back-colour")[0]); + + let message = {'name':msg_name, 'text':message_text, 'text_colour':text_colour, 'size':text_size, 'background_colour':background_colour}; + messages.push(message); + + +}); + + + +function getMessageSettings(page) { + + let messages_el = page.querySelectorAll(".message-list")[0]; + let message_names = getListItems(messages_el); + + //remove any messages that arent in the list + let new_messages = []; + + for (let i = 0; i < messages.length; i++){ + if (message_names.includes(messages[i]['name'])) { + new_messages.push(messages[i]); + } + } + let title = page.querySelectorAll(".title-select")[0].checked; + return {'title':title, 'messages':new_messages}; } diff --git a/stockTicker.py b/stockTicker.py index 5a8a9da..bdb175e 100644 --- a/stockTicker.py +++ b/stockTicker.py @@ -45,7 +45,7 @@ class StockTicker(): self.blank = Image.new('RGB', (10, 32)) self.running = True self.brightness = 1.0 - self.delay = 0.02 + # Configuration for the matrix options = RGBMatrixOptions() @@ -62,7 +62,7 @@ class StockTicker(): 'Daily Forecast':self.getDailyWeatherImage, 'Current Weather': self.getTodayWeatherImage, 'Sports (Team Stats)':lambda : self.getLeagueTableImage('premier_league'), 'Sports (Past Games)': lambda:self.getLeagueImage('NBA', 'past'), 'Sports (Upcoming Games)': lambda : self.getLeagueImage('NHL', 'future'), 'Sports (Live Games)': lambda: self.getLeagueImage('NBA', 'live'), - 'News':self.getNewsImage, 'Custom Messages': self.getUserText, 'Custom Images': self.getUserImage, 'Custom GIFs':self.getUserGIF, + 'News':self.getNewsImage, 'Custom Messages': self.getUserMessages, 'Stocks Prof': self.getStockProfessional, 'Crypto Prof': self.getCryptoProfessional, 'Forex Prof': self.getForexProfessional, @@ -74,6 +74,7 @@ class StockTicker(): def openImage(self, image_file): image = Image.open(image_file) + image = image.convert('RGB') # Make image fit our screen. #image.thumbnail((self.matrix.width, self.matrix.height), Image.ANTIALIAS) @@ -273,9 +274,10 @@ class StockTicker(): def updateMultiple(self, options): for option in options: - img = self.functions[option]() - if option not in ['Custom GIFs']: # aving the gif like this kills the animation + + if option not in ['Custom GIFs', 'Custom Images', 'Custom Messages']: # these images are already saved in user uploads, dodnt need to update them + img = self.functions[option]() img.save('./display_images/'+ option+ '.ppm') @@ -317,69 +319,84 @@ class StockTicker(): update_process = Process(target = self.updateMultiple, args = ([options[(i+1) % len(options)]],)) update_process.start() + self.delay = 0.02 + if options[i % len(options)] == 'Custom Images': + images = self.getUserImages() + + + elif options[i % len(options)] == 'Custom GIFs': + images = self.getUserGIFs() + + elif options[i % len(options)] == 'Custom Messages': + images = self.getUserMessages() + - if options[i % len(options)] != 'Custom GIFs': + else: #these options just have a single ppm image image = self.openImage('./display_images/' + options[i % len(options)] +'.ppm') - image = image.convert('RGB') - else: - image = self.openImage('./display_images/Custom GIFs.gif') + + images = [image] + - img_width, img_height = image.size - - offset_x = 0 - if animation == 'traditional': - offset_x = 128 - elif animation == 'continuous': + for image in images: + img_width, img_height = image.size + offset_x = 0 - elif animation in ['up', 'down']: - offset_x = max(0, 128-img_width) - - offset_y = 0 - #first scroll image in from bottom - - frame_skip = int((1/15)/self.delay) #controls how fast gifs run - self.frame = 0 - - pause_frames = int(0.5/self.delay) - if animation == 'up': - offset_y = 33 - direction = -1 - kill = self.scrollImageY(image, direction = direction, offset_x = offset_x, offset_y = offset_y, frame_skip = frame_skip, gif = options[i % len(options)] == 'Custom GIFs') - elif animation == 'down': - direction = 1 - offset_y = -33 - kill = self.scrollImageY(image, direction = direction, offset_x = offset_x, offset_y = offset_y, frame_skip = frame_skip, gif = options[i % len(options)] == 'Custom GIFs') + if animation == 'traditional': + offset_x = 128 + elif animation == 'continuous': + offset_x = 0 + elif animation in ['up', 'down']: + offset_x = max(0, 128-img_width) - if kill: break - offset_y = 0 - - - if animation in ['up', 'down']: - while pause_frames > 0: - if pause_frames%frame_skip == 0: - self.incrementGIF(image) - - pause_frames -=1 - if options[i % len(options)] != 'Custom GIFs': - self.double_buffer.SetImage(image, offset_x, offset_y) - else: - self.double_buffer.SetImage(image.convert('RGB'), offset_x, offset_y) + offset_y = 0 + #first scroll image in from bottom - self.double_buffer = self.matrix.SwapOnVSync(self.double_buffer) + + + frame_skip = int((1/15)/self.delay) #controls how fast gifs run + self.frame = 0 + + pause_frames = int(0.5/self.delay) + if animation == 'up': + offset_y = 33 + direction = -1 + kill = self.scrollImageY(image, direction = direction, offset_x = offset_x, offset_y = offset_y, frame_skip = frame_skip, gif = options[i % len(options)] == 'Custom GIFs') + elif animation == 'down': + direction = 1 + offset_y = -33 + kill = self.scrollImageY(image, direction = direction, offset_x = offset_x, offset_y = offset_y, frame_skip = frame_skip, gif = options[i % len(options)] == 'Custom GIFs') + + if kill: break + offset_y = 0 + + + if animation in ['up', 'down']: + while pause_frames > 0: + if pause_frames%frame_skip == 0: + self.incrementGIF(image) + + pause_frames -=1 + if options[i % len(options)] != 'Custom GIFs': + self.double_buffer.SetImage(image, offset_x, offset_y) + else: + self.double_buffer.SetImage(image.convert('RGB'), offset_x, offset_y) + + self.double_buffer = self.matrix.SwapOnVSync(self.double_buffer) + + time.sleep(self.delay) + kill = self.checkKilled() + if kill: break + - time.sleep(self.delay) - kill = self.checkKilled() if kill: break - - + + if kill: break - - if kill: break - - kill = self.scrollImage(image, offset_x = offset_x, offset_y = offset_y, frame_skip = frame_skip, gif = options[i % len(options)] == 'Custom GIFs') - - if kill: break + kill = self.scrollImage(image, offset_x = offset_x, offset_y = offset_y, frame_skip = frame_skip, gif = options[i % len(options)] == 'Custom GIFs') + + if kill: break + if kill:break update_process.join() i+=1 @@ -458,7 +475,7 @@ class StockTicker(): if kill: break i+=1 - def textImage(self, text, font, r = 255, g = 255, b = 255, matrix_height = False, w_buff = 3, h_buff = 3): + def textImage(self, text, font, r = 255, g = 255, b = 255, matrix_height = False, w_buff = 3, h_buff = 3, background = False): ''' creates and returns a ppm image containing the text in the supplied font and colour ''' @@ -469,29 +486,55 @@ class StockTicker(): height = 32 img = Image.new('RGB', (width + w_buff, height + h_buff)) d = ImageDraw.Draw(img) + + if background: + br, bg, bb = background + d.rectangle(xy = (0, 0, width + w_buff, height + h_buff), + fill = (br, bg, bb)) + #outline = (255, 255, 255), + #width = 0) #use outline and width to add a border d.text((0, 0), text, fill=(r, g, b), font=font) return img - def getUserText(self): + def getUserMessages(self): ''' displays the text entered in the webpage by the user. ''' - f = open('csv/scroll_text.csv', 'r') - CSV = csv.reader(f) - text, r, g, b = next(CSV) + f = open('csv/message_settings.json', 'r') + all_settings = json.load(f) f.close() - title_img = self.openImage('feature_titles/message.png') - - font = ImageFont.load("./fonts/texgyre-27.pil") + if all_settings['title']: + title_img = self.openImage('feature_titles/message.png') + imgs = [title_img] + else: + imgs = [] - img = self.textImage(text, font, int(r), int(g), int(b), True, w_buff = 50) - return self.stitchImage([title_img, img]) + colours = {'Black':(0,0,0), + 'White':(255,255,255), + 'Red':(255,0,0), + 'Green':(0,255,0), + 'Blue':(0,0,255), + 'Purple':(255,0,255), + 'Yellow':(255,255,0), + 'Cyan':(0,255,255)} + + for message in all_settings['messages']: + font = ImageFont.load("./fonts/texgyre-27.pil") + r,g,b = colours[message['text_colour']] + + background = colours[message['background_colour']] + img = self.textImage(message['text'], font, int(r), int(g), int(b), True, w_buff = 50, background = background) + + imgs.append(img) + + + return imgs def displayGIF(self, gif): # To iterate through the entire gif @@ -1937,7 +1980,7 @@ class StockTicker(): time.sleep(self.delay*1.1) - def getUserImage(self): + def getUserImages(self): title_img = self.openImage('feature_titles/images.png') @@ -1950,21 +1993,30 @@ class StockTicker(): imgs = [] for image in all_settings['images']: - image = self.openImage(os.path.join(os.path.dirname(os.path.abspath(__file__)), image)) + img = self.openImage(os.path.join(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'user_uploads'), image)) + imgs.append(img) - return self.stitchImage(imgs) - - def getUserGIF(self): - title_img = self.openImage('feature_titles/gifs.png') - - gif = Image.open(os.path.join(os.path.dirname(os.path.abspath(__file__)), all_settings['images'][0])) + return imgs + def getUserGIFs(self): f = open('csv/GIF_settings.json', 'r') all_settings = json.load(f) f.close() + if all_settings['title']: + title_img = self.openImage('feature_titles/gifs.png') + GIFs = [title_img] + else: + GIFs = [] + + for f in all_settings['images']: + GIF = Image.open(os.path.join(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'user_uploads'), f)) + GIFs.append(GIF) + + #below code stitches title and GIF together + ''' frames = [] for i, frame in enumerate(ImageSequence.Iterator(gif)): @@ -1977,9 +2029,9 @@ class StockTicker(): frames[0].save('./display_images/Custom GIFs.gif', save_all=True, append_images=frames[1:], loop=0, optimize = False) - + ''' - return None + return GIFs def displayStocks(self): diff --git a/templates/index.html b/templates/index.html index 809f067..930cd2d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2301,12 +2301,13 @@ - + +
@@ -2360,10 +2364,15 @@
- + + + + + + + +
@@ -2375,14 +2384,16 @@
-
- + + +
@@ -2405,10 +2417,15 @@
- + + + - - + + + +
@@ -2421,7 +2438,7 @@
    -
  • Custom Message 1
  • -
  • Custom Message 1
  • -
  • Custom Message 1
  • - -
  • Custom Message 1
  • -
  • Custom Message 1
  • -
  • Custom Message 1
  • - -
  • Custom Message 1
  • -
  • Custom Message 1
  • -
  • Custom Message 1
  • - -
  • Custom Message 1
  • -
  • Custom Message 1
  • -
  • Custom Message 1
  • + +
diff --git a/user_uploads/close.gif b/user_uploads/close.gif new file mode 100644 index 0000000000000000000000000000000000000000..1eadae40c4dc16d28234c6cb4e150b88c535af93 GIT binary patch literal 821 zcmZ?wbhEHb3}R4V_{abTcCTk}KVj&5t@x9L(+DV{_@CR)H6+;CF~HSG&w!Z`BIKEy zn4apJn4X!O&6}Q>o118Bpl4!gVrt5u12P3<9s|?=mVQp9SNx0TOp)~7d~f#FqpgoB zYG*uBz30A7sC#a<@$z?X4}4Di`sUN&*04Py8zYz$n+~0OchbbL(Rbq6<-r%1g`{qs zv{#|qUF1D?+^Wf*vv<@wIj8J5F!d`x{XFrO1>1_P)7iE>-Rdl6{vxDe>yjN6%tys` za@-7^`Ljac!psYj4GdOXTCXytvK)5(+F8~0N!Vb*Da+HE))Q7+duw~I&f?y)`}Zx& zIlrYJ{+8r<@cPys9jUijcTS)7QjD#h`$pz2&zrRy_R1#xyX(t0FXrDVUi+pvIX$`h z{&{uvdETe?t;>sYXuPv2^R;=7!oI36dv;oX^NZXc_AK-LEBetm3q47Mo9TO1`%LHAR>f_&~F6B!e`n449));p8W^Mgx~